├── .github └── workflows │ ├── lint.yml │ ├── readme.yml │ └── test.yml ├── .gitignore ├── .lia.cache ├── .nia.cache ├── LICENSE ├── README.md ├── coqpyt ├── __init__.py ├── coq │ ├── __init__.py │ ├── base_file.py │ ├── changes.py │ ├── context.py │ ├── exceptions.py │ ├── lsp │ │ ├── __init__.py │ │ ├── client.py │ │ └── structs.py │ ├── proof_file.py │ └── structs.py ├── lsp │ ├── __init__.py │ ├── client.py │ ├── endpoint.py │ ├── json_rpc_endpoint.py │ └── structs.py └── tests │ ├── __init__.py │ ├── conftest.py │ ├── proof_file │ ├── expected │ │ ├── imports.yml │ │ ├── list_notation.yml │ │ └── valid_file.yml │ ├── test_cache.py │ ├── test_changes.py │ ├── test_proof_file.py │ └── utility.py │ ├── resources │ ├── .gitignore │ ├── test test │ │ └── test_error.v │ ├── test_add_open_proof.v │ ├── test_bullets.v │ ├── test_change_empty.v │ ├── test_change_goals.v │ ├── test_change_obligation.v │ ├── test_change_with_notation.v │ ├── test_delete_qed.v │ ├── test_derive.v │ ├── test_equations.v │ ├── test_exists_notation.v │ ├── test_get_notation.v │ ├── test_goal.v │ ├── test_imports │ │ ├── .gitignore │ │ ├── Makefile │ │ ├── _CoqProject │ │ ├── test_import.v │ │ └── test_import2.v │ ├── test_imports_copy │ │ ├── .gitignore │ │ ├── Makefile │ │ ├── _CoqProject │ │ ├── test_import.v │ │ └── test_import2.v │ ├── test_invalid_1.v │ ├── test_invalid_2.v │ ├── test_invalid_changes.v │ ├── test_list_notation.v │ ├── test_module_inline.v │ ├── test_module_type.v │ ├── test_nested_proofs.v │ ├── test_non_ending_proof.v │ ├── test_nth_locate.v │ ├── test_obligation.v │ ├── test_proof_cmd.v │ ├── test_section_terms.v │ ├── test_simple_file.v │ ├── test_theorem_tokens.v │ ├── test_type_class.v │ ├── test_unknown_notation.v │ ├── test_valid.v │ └── test_where_notation.v │ ├── test_cache.py │ ├── test_context.py │ ├── test_coq_file.py │ ├── test_coq_lsp_client.py │ ├── test_json_rpc_endpoint.py │ └── test_readme.py ├── examples ├── readme.py └── readme.v ├── images ├── logo.png └── uml.png ├── requirements.txt └── setup.py /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3.5.2 15 | 16 | - name: Run linter 17 | uses: psf/black@stable 18 | with: 19 | options: "--check --verbose" 20 | version: "23.3.0" -------------------------------------------------------------------------------- /.github/workflows/readme.yml: -------------------------------------------------------------------------------- 1 | name: Embed code in README 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - "examples/readme.py" 9 | - "README.md" 10 | pull_request_target: 11 | types: 12 | - opened 13 | - edited 14 | paths: 15 | - "examples/readme.py" 16 | - "README.md" 17 | 18 | jobs: 19 | embed: 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v3.5.2 25 | 26 | - name: Set up node 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: 20 30 | 31 | - name: Install embedme 32 | run: npm install -g embedme 33 | 34 | - name: Embed code 35 | run: embedme README.md 36 | 37 | - name: Commit changes 38 | uses: EndBug/add-and-commit@v9 39 | with: 40 | add: "README.md" 41 | push: origin ${{ github.head_ref }} 42 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | ocaml-compiler: 15 | - "5.2.0" 16 | coq-version: 17 | - "8.17.1" 18 | - "8.18.0" 19 | - "8.19.2" 20 | - "8.20.0" 21 | 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v3.5.2 25 | 26 | - name: Set up Python 27 | uses: actions/setup-python@v4.6.1 28 | with: 29 | python-version: '3.11' 30 | 31 | - name: Install Python dependencies 32 | run: | 33 | python -m pip install --upgrade pip 34 | pip install -r requirements.txt 35 | 36 | - name: Restore opam 37 | id: cache-opam-restore 38 | uses: actions/cache/restore@v4 39 | with: 40 | path: | 41 | /home/runner/work/coqpyt/coqpyt/_opam/ 42 | key: ${{ matrix.ocaml-compiler }}-${{ matrix.coq-version }}-opam 43 | 44 | - name: Set-up OCaml 45 | uses: ocaml/setup-ocaml@v3 46 | with: 47 | ocaml-compiler: ${{ matrix.ocaml-compiler }} 48 | 49 | - name: Install coq-lsp 50 | if: steps.cache-opam-restore.outputs.cache-hit != 'true' 51 | run: | 52 | opam pin add coq ${{ matrix.coq-version }} 53 | opam install coq-lsp 54 | 55 | - name: Add coq-released 56 | if: steps.cache-opam-restore.outputs.cache-hit != 'true' 57 | run: | 58 | opam repo add coq-released https://coq.inria.fr/opam/released 59 | 60 | - name: Install coq-equations 61 | if: steps.cache-opam-restore.outputs.cache-hit != 'true' 62 | run: | 63 | opam install coq-equations 64 | 65 | - name: Install coqpyt 66 | run: | 67 | pip install -e . 68 | 69 | - name: Run tests 70 | run: | 71 | eval $(opam env) 72 | cd coqpyt 73 | pytest tests -s --runextra 74 | 75 | - name: Save opam 76 | id: cache-opam-save 77 | uses: actions/cache/save@v4 78 | with: 79 | path: | 80 | /home/runner/work/coqpyt/coqpyt/_opam/ 81 | key: ${{ steps.cache-opam-restore.outputs.cache-primary-key }} 82 | -------------------------------------------------------------------------------- /.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 | # Editor 107 | .vscode/ -------------------------------------------------------------------------------- /.lia.cache: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sr-lab/coqpyt/75a3cca35f6d0f4043b94c26948afde8869ebd77/.lia.cache -------------------------------------------------------------------------------- /.nia.cache: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sr-lab/coqpyt/75a3cca35f6d0f4043b94c26948afde8869ebd77/.nia.cache -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Avi Yeger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](https://github.com/sr-lab/coqpyt/blob/master/images/logo.png?raw=true) 2 | 3 | Interact with Coq files and navigate through your proofs using our Python client for [coq-lsp](https://github.com/ejgallego/coq-lsp). 4 | 5 | Execute Coq files, retrieve the generated context and edit proofs through insertion and removal of steps. 6 | 7 | If you use CoqPyt in an article, please cite: 8 | 9 | [CoqPyt: Proof Navigation in Python in the Era of LLMs](https://doi.org/10.1145/3663529.3663814) 10 | 11 | ``` 12 | @inproceedings{carrott2024coqpyt, 13 | title={CoqPyt: Proof Navigation in Python in the Era of LLMs}, 14 | author={Carrott, Pedro and Saavedra, Nuno and Thompson, Kyle and Lerner, Sorin and Ferreira, Jo{\~a}o F and First, Emily}, 15 | booktitle={Companion Proceedings of the 32nd ACM International Conference on the Foundations of Software Engineering}, 16 | pages={637--641}, 17 | year={2024} 18 | } 19 | ``` 20 | 21 | ## Installation 22 | 23 | [coq-lsp](https://github.com/ejgallego/coq-lsp) must be installed on version >= 0.1.7. Follow the installation instructions provided [here](https://github.com/ejgallego/coq-lsp#%EF%B8%8F-installation). 24 | 25 | ```bash 26 | pip install -r requirements.txt 27 | ``` 28 | 29 | ```bash 30 | python -m pip install -e . 31 | ``` 32 | 33 | ## Usage 34 | 35 | ![UML](https://github.com/sr-lab/coqpyt/blob/master/images/uml.png?raw=true) 36 | 37 | Import classes from the ``coqpyt`` package. 38 | 39 | 40 | ```py 41 | from coqpyt.coq.structs import TermType 42 | from coqpyt.coq.base_file import CoqFile 43 | from coqpyt.coq.proof_file import ProofFile 44 | from coqpyt.coq.changes import ProofAppend, ProofPop 45 | from coqpyt.coq.exceptions import InvalidChangeException 46 | ``` 47 | 48 | ### Interaction with Coq 49 | 50 | Create a CoqFile object, execute the file and extract the generated context. 51 | 52 | 53 | ```py 54 | # Open Coq file 55 | with CoqFile(os.path.join(os.getcwd(), "examples/readme.v")) as coq_file: 56 | coq_file.exec(nsteps=2) 57 | # Get all terms defined until now 58 | print("Number of terms:", len(coq_file.context.terms)) 59 | # Filter by Tactics 60 | print( 61 | "Number of tactics:", 62 | len( 63 | list( 64 | filter( 65 | lambda term: term.type == TermType.TACTIC, 66 | coq_file.context.terms.values(), 67 | ) 68 | ) 69 | ), 70 | ) 71 | 72 | # Save compiled file 73 | coq_file.save_vo() 74 | print("Compiled file exists:", os.path.exists("examples/readme.vo")) 75 | os.remove("examples/readme.vo") 76 | 77 | # Run remaining file 78 | coq_file.run() 79 | print("Checked:", coq_file.checked) 80 | # Get all terms defined until now 81 | print("Number of terms:", len(coq_file.context.terms)) 82 | ``` 83 | 84 | Create a ProofFile object (a CoqFile instance) and interact with the proofs. 85 | 86 | 87 | ```py 88 | # Open Proof file 89 | with ProofFile(os.path.join(os.getcwd(), "examples/readme.v")) as proof_file: 90 | # Enter proof 91 | proof_file.exec(nsteps=4) 92 | print("In proof:", proof_file.in_proof) 93 | # Get current goals 94 | print(proof_file.current_goals) 95 | 96 | # Run remaining file 97 | proof_file.run() 98 | # Number of proofs in the file 99 | print("Number of proofs:", len(proof_file.proofs)) 100 | print("Proof:", proof_file.proofs[0].text) 101 | 102 | # Print steps of proof 103 | for step in proof_file.proofs[0].steps: 104 | print(step.text, end="") 105 | print() 106 | 107 | # Get the context used in the third step 108 | print(proof_file.proofs[0].steps[2].context) 109 | # Print the goals in the third step 110 | print(proof_file.proofs[0].steps[2].goals) 111 | 112 | # Print number of terms in context 113 | print("Number of terms:", len(proof_file.context.terms)) 114 | # Filter for Notations only 115 | print( 116 | "Number of notations:", 117 | len( 118 | list( 119 | filter( 120 | lambda term: term.type == TermType.NOTATION, 121 | proof_file.context.terms.values(), 122 | ) 123 | ) 124 | ), 125 | ) 126 | ``` 127 | 128 | ### Proof Modification 129 | 130 | Given an admitted proof: 131 | 132 | 133 | ```coq 134 | Lemma rev_append: forall {a} (l1 l2: list a), 135 | rev (l1 ++ l2) = rev l2 ++ rev l1. 136 | Proof. 137 | intros a l1 l2. induction l1; intros. 138 | - simpl. rewrite app_nil_r. reflexivity. 139 | - simpl. rewrite IHl1. 140 | Admitted. 141 | ``` 142 | 143 | Perform step-wise changes to the proof. 144 | 145 | 146 | ```py 147 | with ProofFile(os.path.join(os.getcwd(), "examples/readme.v")) as proof_file: 148 | proof_file.run() 149 | # Get the first admitted proof 150 | unproven = proof_file.unproven_proofs[0] 151 | # Steps for an incorrect proof 152 | incorrect = [" reflexivity.", "\nQed."] 153 | # Steps for a correct proof 154 | correct = [" rewrite app_assoc."] + incorrect 155 | 156 | # Loop through both attempts 157 | for attempt in [incorrect, correct]: 158 | # Remove the "\nAdmitted." step 159 | proof_file.pop_step(unproven) 160 | try: 161 | # Append all steps in the attempt 162 | for i, s in enumerate(attempt): 163 | proof_file.append_step(unproven, s) 164 | print("Proof succeeded!") 165 | break 166 | except InvalidChangeException: 167 | # Some step was invalid, so we rollback the previous changes 168 | [proof_file.pop_step(unproven) for _ in range(i)] 169 | proof_file.append_step(unproven, "\nAdmitted.") 170 | print("Proof attempt not valid.") 171 | ``` 172 | 173 | Perform changes to the proof transactionally. 174 | 175 | 176 | ```py 177 | with ProofFile(os.path.join(os.getcwd(), "examples/readme.v")) as proof_file: 178 | proof_file.run() 179 | # Get the first admitted proof 180 | unproven = proof_file.unproven_proofs[0] 181 | # Steps for an incorrect proof 182 | incorrect = [" reflexivity.", "\nQed."] 183 | # Steps for a correct proof 184 | correct = [" rewrite app_assoc."] + incorrect 185 | 186 | # Loop through both attempts 187 | for attempt in [incorrect, correct]: 188 | # Schedule the removal of the "\nAdmitted." step 189 | changes = [ProofPop()] 190 | # Schedule the addition of each step in the attempt 191 | for s in attempt: 192 | changes.append(ProofAppend(s)) 193 | try: 194 | # Apply all changes in one batch 195 | proof_file.change_proof(unproven, changes) 196 | print("Proof succeeded!") 197 | break 198 | except InvalidChangeException: 199 | # Some batch of changes was invalid 200 | # Rollback is automatic, so no rollback needed 201 | print("Proof attempt not valid.") 202 | ``` 203 | 204 | ## Tests 205 | 206 | To run the core tests for CoqPyt go to the folder ``coqpyt`` and run: 207 | ```bash 208 | pytest tests -rs 209 | ``` 210 | 211 | Skipped tests require the following external Coq libraries to run successfully: 212 | - [Coq-Equations](https://github.com/mattam82/Coq-Equations) 213 | 214 | To run all tests go to the folder ``coqpyt`` and run: 215 | ```bash 216 | pytest tests -rs --runextra 217 | ``` 218 | 219 | ## Contributing 220 | 221 | Pull requests are welcome. 222 | 223 | For major changes, please open an issue first to discuss what you would like to change. 224 | 225 | Please make sure to update tests as appropriate. 226 | 227 | ## Credits 228 | 229 | Special thanks to the developers of the [pylspclient](https://github.com/yeger00/pylspclient) project, which served as the initial template for CoqPyt. Additionally, we express our gratitude to [Kyle Thompson](https://github.com/rkthomps/) for his precious feedback, which has greatly contributed to the refinement of CoqPyt. 230 | 231 | ## License 232 | 233 | [MIT](https://choosealicense.com/licenses/mit/) 234 | -------------------------------------------------------------------------------- /coqpyt/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sr-lab/coqpyt/75a3cca35f6d0f4043b94c26948afde8869ebd77/coqpyt/__init__.py -------------------------------------------------------------------------------- /coqpyt/coq/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sr-lab/coqpyt/75a3cca35f6d0f4043b94c26948afde8869ebd77/coqpyt/coq/__init__.py -------------------------------------------------------------------------------- /coqpyt/coq/base_file.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import uuid 4 | import tempfile 5 | from copy import deepcopy 6 | from typing import Optional, List, Tuple 7 | 8 | from coqpyt.lsp.structs import ( 9 | TextDocumentItem, 10 | TextDocumentIdentifier, 11 | VersionedTextDocumentIdentifier, 12 | TextDocumentContentChangeEvent, 13 | ResponseError, 14 | ErrorCodes, 15 | Diagnostic, 16 | ) 17 | from coqpyt.coq.lsp.structs import Position, RangedSpan, Range 18 | from coqpyt.coq.lsp.client import CoqLspClient 19 | from coqpyt.coq.exceptions import * 20 | from coqpyt.coq.changes import * 21 | from coqpyt.coq.structs import Step 22 | from coqpyt.coq.context import FileContext 23 | 24 | 25 | class CoqFile(object): 26 | """Abstraction to interact with a Coq file 27 | 28 | Attributes: 29 | coq_lsp_client (CoqLspClient): coq-lsp client used on the file 30 | ast (List[RangedSpan]): AST of the Coq file. Each element is a step 31 | of execution in the Coq file. 32 | steps_taken (int): The number of steps already executed 33 | context (FileContext): The context defined in the file. 34 | path (str): Path of the file. If the file is from the Coq library, a 35 | temporary file will be used. 36 | file_module(List[str]): Module where the file is included. 37 | """ 38 | 39 | def __init__( 40 | self, 41 | file_path: str, 42 | library: Optional[str] = None, 43 | timeout: int = 30, 44 | workspace: Optional[str] = None, 45 | coq_lsp: str = "coq-lsp", 46 | coq_lsp_options: Tuple[str] = None, 47 | coqtop: str = "coqtop", 48 | ): 49 | """Creates a CoqFile. 50 | 51 | Args: 52 | file_path (str): Path of the Coq file. 53 | library (Optional[str], optional): The library of the file. Defaults to None. 54 | timeout (int, optional): Timeout used in coq-lsp. Defaults to 2. 55 | workspace(Optional[str], optional): Absolute path for the workspace. 56 | If the workspace is not defined, the workspace is equal to the 57 | path of the file. 58 | coq_lsp(str, optional): Path to the coq-lsp binary. Defaults to "coq-lsp". 59 | coqtop(str, optional): Path to the coqtop binary used to compile the Coq libraries 60 | imported by coq-lsp. This is NOT passed as a parameter to coq-lsp, it is 61 | simply used to check the Coq version in use. Defaults to "coqtop". 62 | """ 63 | if not os.path.isabs(file_path): 64 | file_path = os.path.abspath(file_path) 65 | self.__init_path(file_path, library) 66 | 67 | if workspace is not None: 68 | uri = f"file://{workspace}" 69 | else: 70 | uri = f"file://{self._path}" 71 | self.coq_lsp_client = CoqLspClient( 72 | uri, timeout=timeout, coq_lsp_options=coq_lsp_options, coq_lsp=coq_lsp 73 | ) 74 | uri = f"file://{self._path}" 75 | text = self.__read() 76 | 77 | try: 78 | self.coq_lsp_client.didOpen(TextDocumentItem(uri, "coq", 1, text)) 79 | ast = self.coq_lsp_client.get_document(TextDocumentIdentifier(uri)).spans 80 | except Exception as e: 81 | self._handle_exception(e) 82 | raise e 83 | 84 | self.steps_taken: int = 0 85 | self.__init_steps(text, ast) 86 | self.__validate() 87 | self.context = FileContext(self.path, module=self.file_module, coqtop=coqtop) 88 | self.version = 1 89 | self.workspace = workspace 90 | 91 | def __enter__(self): 92 | return self 93 | 94 | def __exit__(self, exc_type, exc_value, traceback): 95 | self.close() 96 | 97 | def __init_path(self, file_path, library): 98 | self.file_module = [] if library is None else library.split(".") 99 | self.__from_lib = self.file_module[:2] == ["Coq", "Init"] 100 | self.path = file_path 101 | if not self.__from_lib: 102 | self._path = file_path 103 | return 104 | 105 | # Coq LSP cannot open files from Coq library, so we need to work with 106 | # a copy of such files. 107 | temp_dir = tempfile.gettempdir() 108 | new_path = os.path.join( 109 | temp_dir, "coq_" + str(uuid.uuid4()).replace("-", "") + ".v" 110 | ) 111 | shutil.copyfile(file_path, new_path) 112 | self._path = new_path 113 | 114 | def _handle_exception(self, e): 115 | if not isinstance(e, ResponseError) or e.code not in [ 116 | ErrorCodes.ServerQuit.value, 117 | ErrorCodes.ServerTimeout.value, 118 | ]: 119 | self.coq_lsp_client.shutdown() 120 | self.coq_lsp_client.exit() 121 | if self.__from_lib: 122 | os.remove(self._path) 123 | 124 | def __init_step( 125 | self, 126 | lines: List[str], 127 | index: int, 128 | step_ast: RangedSpan, 129 | prev_step_ast: RangedSpan, 130 | ): 131 | start_line = 0 if index == 0 else prev_step_ast.range.end.line 132 | start_char = 0 if index == 0 else prev_step_ast.range.end.character 133 | end_line = step_ast.range.end.line 134 | end_char = step_ast.range.end.character 135 | 136 | curr_lines = lines[start_line : end_line + 1] 137 | curr_lines[-1] = curr_lines[-1][:end_char] 138 | curr_lines[0] = curr_lines[0][start_char:] 139 | step_text = "\n".join(curr_lines) 140 | 141 | if index == 0: 142 | short_text = self.__short_text(step_text, step_ast) 143 | else: 144 | short_text = self.__short_text(step_text, step_ast, prev_step_ast) 145 | 146 | return Step(step_text, short_text, step_ast) 147 | 148 | def __init_steps(self, text: str, ast: List[RangedSpan]): 149 | lines = text.split("\n") 150 | self.steps: List[Step] = [] 151 | # NOTE: We remove the last step if it is an empty step 152 | if ast[-1].span == None: 153 | ast = ast[:-1] 154 | for i, curr_ast in enumerate(ast): 155 | self.steps.append(self.__init_step(lines, i, curr_ast, ast[i - 1])) 156 | 157 | def __validate(self): 158 | uri = f"file://{self._path}" 159 | self.is_valid = True 160 | if uri not in self.coq_lsp_client.lsp_endpoint.diagnostics: 161 | return 162 | 163 | for diagnostic in self.coq_lsp_client.lsp_endpoint.diagnostics[uri]: 164 | if diagnostic.severity == 1: 165 | self.is_valid = False 166 | 167 | for step in self.steps: 168 | if ( 169 | step.ast.range.start <= diagnostic.range.start 170 | and step.ast.range.end >= diagnostic.range.end 171 | ): 172 | step.diagnostics.append(diagnostic) 173 | break 174 | 175 | def __short_text( 176 | self, text: str, curr_step: RangedSpan, prev_step: Optional[RangedSpan] = None 177 | ): 178 | curr_range = curr_step.range 179 | nlines = curr_range.end.line - curr_range.start.line + 1 180 | lines = text.split("\n")[-nlines:] 181 | 182 | start = curr_range.start.character 183 | if prev_step is not None and prev_step.range.end.line >= curr_range.start.line: 184 | start -= prev_step.range.end.character 185 | 186 | lines[-1] = lines[-1][: curr_range.end.character] 187 | lines[0] = lines[0][start:] 188 | 189 | return " ".join(" ".join(lines).split()) 190 | 191 | def __read(self): 192 | with open(self.path, "r") as f: 193 | return f.read() 194 | 195 | def __refresh(self): 196 | uri = f"file://{self.path}" 197 | text = self.__read() 198 | try: 199 | self.version += 1 200 | self.coq_lsp_client.didChange( 201 | VersionedTextDocumentIdentifier(uri, self.version), 202 | [TextDocumentContentChangeEvent(None, None, text)], 203 | ) 204 | except Exception as e: 205 | self._handle_exception(e) 206 | raise e 207 | 208 | def __update_steps(self): 209 | self.__refresh() 210 | uri = f"file://{self.path}" 211 | text = self.__read() 212 | try: 213 | ast = self.coq_lsp_client.get_document(TextDocumentIdentifier(uri)).spans 214 | except Exception as e: 215 | self._handle_exception(e) 216 | raise e 217 | self.__init_steps(text, ast) 218 | self.__validate() 219 | 220 | def _step(self, sign): 221 | if sign == 1: 222 | self.context.process_step(self.curr_step) 223 | else: 224 | self.context.undo_step(self.prev_step) 225 | self.steps_taken += sign 226 | 227 | def _make_change(self, change_function, *args): 228 | uri = f"file://{self._path}" 229 | if not self.is_valid: 230 | raise InvalidFileException(self.path) 231 | 232 | self.__set_backup_steps() 233 | old_steps_taken = self.steps_taken 234 | old_diagnostics = self.coq_lsp_client.lsp_endpoint.diagnostics[uri] 235 | self.coq_lsp_client.lsp_endpoint.diagnostics[uri] = [] 236 | old_text = self.__read() 237 | 238 | try: 239 | change_function(*args) 240 | except InvalidChangeException as e: 241 | # Rollback changes 242 | self.steps = self.__backup_steps 243 | self.steps_taken = old_steps_taken 244 | self.is_valid = True 245 | with open(self.path, "w") as f: 246 | f.write(old_text) 247 | e.diagnostics = self.coq_lsp_client.lsp_endpoint.diagnostics[uri] 248 | self.__refresh() 249 | self.coq_lsp_client.lsp_endpoint.diagnostics[uri] = old_diagnostics 250 | raise e 251 | 252 | def __delete_step_text(self, step_index: int): 253 | with open(self._path, "r") as f: 254 | lines = f.readlines() 255 | 256 | step = self.steps[step_index] 257 | if step_index != 0: 258 | prev_step_end = self.steps[step_index - 1].ast.range.end 259 | else: 260 | prev_step_end = Position(0, 0) 261 | 262 | start_line = lines[prev_step_end.line] 263 | end_line = lines[step.ast.range.end.line] 264 | 265 | end_line = end_line[step.ast.range.end.character :] 266 | start_line = start_line[: prev_step_end.character] 267 | 268 | if prev_step_end.line == step.ast.range.end.line: 269 | lines[prev_step_end.line] = start_line + end_line 270 | else: 271 | lines[prev_step_end.line] = start_line 272 | lines[step.ast.range.end.line] = end_line 273 | 274 | # Delete lines between first and last line 275 | for _ in range(step.ast.range.end.line - prev_step_end.line - 1): 276 | del lines[prev_step_end.line + 1] 277 | text = "".join(lines) 278 | 279 | with open(self._path, "w") as f: 280 | f.write(text) 281 | 282 | def __add_step_text(self, previous_step_index: int, step_text: str): 283 | with open(self._path, "r") as f: 284 | lines = f.readlines() 285 | 286 | previous_step = self.steps[previous_step_index] 287 | end_line = lines[previous_step.ast.range.end.line] 288 | end_line = ( 289 | end_line[: previous_step.ast.range.end.character] 290 | + step_text 291 | + end_line[previous_step.ast.range.end.character :] 292 | ) 293 | lines[previous_step.ast.range.end.line] = end_line 294 | 295 | text = "".join(lines) 296 | 297 | with open(self._path, "w") as f: 298 | f.write(text) 299 | 300 | def __delete_update_ast(self, step_index: int): 301 | deleted_step = self.steps[step_index] 302 | if step_index != 0: 303 | prev_step_end = self.steps[step_index - 1].ast.range.end 304 | else: 305 | prev_step_end = Position(0, 0) 306 | 307 | deleted_lines = deleted_step.text.count("\n") 308 | last_line_chars = deleted_step.ast.range.end.character 309 | last_line_offset = 0 310 | if deleted_step.ast.range.start.line == prev_step_end.line: 311 | last_line_chars -= prev_step_end.character 312 | else: 313 | last_line_offset = prev_step_end.character 314 | 315 | for step in self.steps[step_index:]: 316 | step.ast.range.start.line -= deleted_lines 317 | step.ast.range.end.line -= deleted_lines 318 | 319 | if step.ast.range.start.line == deleted_step.ast.range.end.line: 320 | step.ast.range.start.character -= last_line_chars - last_line_offset 321 | if step.ast.range.end.line == deleted_step.ast.range.end.line: 322 | step.ast.range.end.character -= last_line_chars - last_line_offset 323 | 324 | def __add_update_ast(self, previous_step_index: int, step_text: str) -> Step: 325 | prev_step_end = self.steps[previous_step_index].ast.range.end 326 | start = Position(prev_step_end.line, prev_step_end.character) 327 | 328 | added_lines = step_text.count("\n") 329 | end_char = last_line_chars = len(step_text.split("\n")[-1]) 330 | if added_lines == 0: 331 | end_char += start.character 332 | end = Position(start.line + added_lines, end_char) 333 | 334 | # We will create a placeholder step that will be replaced later 335 | added_step = Step(step_text, step_text, RangedSpan(Range(start, end), None)) 336 | 337 | for step in self.steps[previous_step_index + 1 :]: 338 | step.ast.range.start.line += added_lines 339 | step.ast.range.end.line += added_lines 340 | 341 | if step.ast.range.start.line == added_step.ast.range.end.line: 342 | step.ast.range.start.character += last_line_chars 343 | if step.ast.range.end.line == added_step.ast.range.end.line: 344 | step.ast.range.end.character += last_line_chars 345 | 346 | return added_step 347 | 348 | def __copy_steps(self): 349 | for i, step in enumerate(self.steps): 350 | index = self.__index_tracker[i] 351 | if index is None: # Newly added steps 352 | continue 353 | 354 | backup = self.__backup_steps[index] 355 | backup.text, backup.ast = step.text, step.ast 356 | backup.diagnostics = step.diagnostics 357 | backup.short_text = step.short_text 358 | self.steps[i] = backup 359 | 360 | def __set_backup_steps(self): 361 | self.__backup_steps = self.steps[:] 362 | self.__index_tracker: List[Optional[int]] = [] 363 | for i, step in enumerate(self.__backup_steps): 364 | self.steps[i] = deepcopy(step) 365 | self.__index_tracker.append(i) 366 | 367 | def _delete_step(self, step_index: int) -> None: 368 | deleted_step = self.steps[step_index] 369 | deleted_text = deleted_step.text 370 | self.__delete_step_text(step_index) 371 | 372 | # Modify the previous steps instead of creating new ones 373 | # This is important to preserve their references 374 | # For instance, in the ProofFile 375 | self.__update_steps() 376 | 377 | if not self.is_valid: 378 | raise InvalidDeleteException(deleted_text) 379 | 380 | # We will remove the step from the previous steps 381 | self.__index_tracker.pop(step_index) 382 | self.__copy_steps() 383 | 384 | if self.steps_taken > step_index: 385 | self.steps_taken -= 1 386 | n_steps = self.steps_taken - step_index 387 | # We don't use self to avoid calling method of ProofFile 388 | CoqFile.exec(self, -n_steps) 389 | self.context.undo_step(deleted_step) 390 | CoqFile.exec(self, n_steps) 391 | 392 | def _add_step(self, previous_step_index: int, step_text: str) -> None: 393 | self.__add_step_text(previous_step_index, step_text) 394 | 395 | # Modify the previous steps instead of creating new ones 396 | # This is important to preserve their references 397 | # For instance, in the ProofFile 398 | previous_steps_size = len(self.steps) 399 | step_index = previous_step_index + 1 400 | self.__update_steps() 401 | 402 | # NOTE: We check if exactly 1 step was added, because the text might contain 403 | # two steps or something that might lead to similar unwanted behaviour. 404 | if len(self.steps) != previous_steps_size + 1 or not self.is_valid: 405 | raise InvalidAddException(step_text) 406 | 407 | # We will add the new step to the previous steps 408 | self.__index_tracker.insert(step_index, None) 409 | self.__copy_steps() 410 | 411 | if self.steps_taken > step_index: 412 | self.steps_taken += 1 413 | n_steps = self.steps_taken - step_index 414 | CoqFile.exec(self, -n_steps + 1) 415 | # Ignore step when going back 416 | self.steps_taken -= 1 417 | CoqFile.exec(self, n_steps) 418 | 419 | def _get_steps_taken_offset(self, changes: List[CoqChange]): 420 | offset = 0 421 | steps_taken = self.steps_taken 422 | 423 | for change in changes: 424 | if isinstance(change, CoqAdd): 425 | if change.previous_step_index + 1 < steps_taken: 426 | offset += 1 427 | steps_taken += 1 428 | elif isinstance(change, CoqDelete): 429 | if change.step_index < steps_taken: 430 | offset -= 1 431 | steps_taken -= 1 432 | 433 | return offset 434 | 435 | def __change_steps(self, changes: List[CoqChange]): 436 | previous_steps_takens = self.steps_taken 437 | offset_steps, offset_steps_taken = 0, self._get_steps_taken_offset(changes) 438 | previous_steps_size = len(self.steps) 439 | CoqFile.exec(self, -self.steps_taken) 440 | 441 | for change in changes: 442 | if isinstance(change, CoqAdd): 443 | self.__add_step_text(change.previous_step_index, change.step_text) 444 | step = self.__add_update_ast( 445 | change.previous_step_index, change.step_text 446 | ) 447 | self.steps.insert(change.previous_step_index + 1, step) 448 | self.__index_tracker.insert(change.previous_step_index + 1, None) 449 | offset_steps += 1 450 | elif isinstance(change, CoqDelete): 451 | self.__delete_step_text(change.step_index) 452 | self.__delete_update_ast(change.step_index) 453 | self.steps.pop(change.step_index) 454 | self.__index_tracker.pop(change.step_index) 455 | offset_steps -= 1 456 | else: 457 | raise NotImplementedError(f"Unknown change: {change}") 458 | 459 | self.__update_steps() 460 | # NOTE: We check the expected offset, because a given step text might contain 461 | # two steps or something that might lead to similar unwanted behaviour. 462 | if len(self.steps) != previous_steps_size + offset_steps or not self.is_valid: 463 | CoqFile.exec(self, previous_steps_takens + offset_steps_taken) 464 | raise InvalidChangeException() 465 | 466 | self.__copy_steps() 467 | CoqFile.exec(self, previous_steps_takens + offset_steps_taken) 468 | 469 | @property 470 | def curr_step(self): 471 | """ 472 | Returns: 473 | Step: The next step to be executed. 474 | """ 475 | return self.steps[self.steps_taken] 476 | 477 | @property 478 | def prev_step(self): 479 | """ 480 | Returns: 481 | Step: The previously executed step. 482 | """ 483 | return self.steps[self.steps_taken - 1] 484 | 485 | @property 486 | def timeout(self) -> int: 487 | """The timeout of the coq-lsp client. 488 | 489 | Returns: 490 | int: Timeout. 491 | """ 492 | return self.coq_lsp_client.lsp_endpoint.timeout 493 | 494 | @property 495 | def checked(self) -> bool: 496 | """ 497 | Returns: 498 | bool: True if the whole file was already executed 499 | """ 500 | return self.steps_taken == len(self.steps) 501 | 502 | @property 503 | def diagnostics(self) -> List[Diagnostic]: 504 | """ 505 | Returns: 506 | List[Diagnostic]: The diagnostics of the file. 507 | Includes all messages given by Coq. 508 | """ 509 | uri = f"file://{self._path}" 510 | return self.coq_lsp_client.lsp_endpoint.diagnostics[uri] 511 | 512 | @property 513 | def errors(self) -> List[Diagnostic]: 514 | """ 515 | Returns: 516 | List[Diagnostic]: The errors of the file. 517 | Includes all messages given by Coq with severity 1. 518 | """ 519 | return list(filter(lambda x: x.severity == 1, self.diagnostics)) 520 | 521 | def exec(self, nsteps=1) -> List[Step]: 522 | """Execute steps in the file. 523 | 524 | Args: 525 | nsteps (int, optional): Number of steps to execute. Defaults to 1. 526 | 527 | Returns: 528 | List[Step]: List of steps executed. 529 | """ 530 | sign = 1 if nsteps > 0 else -1 531 | initial_steps_taken = self.steps_taken 532 | nsteps = min( 533 | nsteps * sign, 534 | len(self.steps) - self.steps_taken if sign > 0 else self.steps_taken, 535 | ) 536 | 537 | for _ in range(nsteps): 538 | self._step(sign) 539 | 540 | last, slice = sign == 1, (initial_steps_taken, self.steps_taken) 541 | return self.steps[slice[1 - last] : slice[last]] 542 | 543 | def run(self) -> List[Step]: 544 | """Executes the remaining steps in the file. 545 | 546 | Returns: 547 | List[Step]: List of the remaining steps in the file. 548 | """ 549 | return self.exec(len(self.steps) - self.steps_taken) 550 | 551 | def delete_step(self, step_index: int) -> None: 552 | """Deletes a step from the file. This function will change the original file. 553 | If an exception is thrown the file will not be changed. 554 | 555 | Args: 556 | step_index (int): The index of the step to remove. 557 | 558 | Raises: 559 | InvalidFileException: If the file being changed is not valid. 560 | InvalidDeleteException: If the file is invalid after deleting the step. 561 | """ 562 | self._make_change(self._delete_step, step_index) 563 | 564 | def add_step( 565 | self, 566 | previous_step_index: int, 567 | step_text: str, 568 | ) -> None: 569 | """Adds a step to the file. This function will change the original file. 570 | If an exception is thrown the file will not be changed. 571 | 572 | Args: 573 | previous_step_index (int): The index of the previous step of the new step. 574 | step_text (str): The text of the step to add. 575 | 576 | Raises: 577 | InvalidFileException: If the file being changed is not valid. 578 | InvalidAddException: If the file is invalid after adding the step. 579 | """ 580 | self._make_change(self._add_step, previous_step_index, step_text) 581 | 582 | def change_steps(self, changes: List[CoqChange]): 583 | """Changes the steps of the original Coq file transactionally. 584 | If an exception is thrown the file will not be changed. 585 | 586 | Args: 587 | changes (List[CoqChange]): The changes to be applied to the Coq file. 588 | 589 | Raises: 590 | InvalidFileException: If the file being changed is not valid. 591 | InvalidChangeException: If the file is invalid after applying the changes. 592 | NotImplementedError: If the changes contain a CoqChange that is not a CoqAdd or CoqDelete. 593 | """ 594 | self._make_change(self.__change_steps, changes) 595 | 596 | def save_vo(self): 597 | """Compiles the vo file for this Coq file.""" 598 | uri = f"file://{self._path}" 599 | try: 600 | self.coq_lsp_client.save_vo(TextDocumentIdentifier(uri)) 601 | except Exception as e: 602 | self._handle_exception(e) 603 | raise e 604 | 605 | def close(self): 606 | """Closes all resources used by this object.""" 607 | self.coq_lsp_client.shutdown() 608 | self.coq_lsp_client.exit() 609 | if self.__from_lib: 610 | os.remove(self._path) 611 | -------------------------------------------------------------------------------- /coqpyt/coq/changes.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | class CoqChange: 5 | pass 6 | 7 | 8 | @dataclass 9 | class CoqAdd(CoqChange): 10 | step_text: str 11 | previous_step_index: int 12 | 13 | 14 | @dataclass 15 | class CoqDelete(CoqChange): 16 | step_index: int 17 | 18 | 19 | class ProofChange: 20 | pass 21 | 22 | 23 | @dataclass 24 | class ProofAppend(ProofChange): 25 | step_text: str 26 | 27 | 28 | @dataclass 29 | class ProofPop(ProofChange): 30 | pass 31 | -------------------------------------------------------------------------------- /coqpyt/coq/exceptions.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from coqpyt.coq.structs import Diagnostic 3 | 4 | 5 | class InvalidChangeException(Exception): 6 | def __init__(self): 7 | self.diagnostics: List[Diagnostic] = [] 8 | 9 | @property 10 | def errors(self) -> List[Diagnostic]: 11 | return [d for d in self.diagnostics if d.severity == 1] 12 | 13 | 14 | class InvalidAddException(InvalidChangeException): 15 | def __init__(self, step: str): 16 | self.step: str = step 17 | 18 | def __str__(self): 19 | return "Adding the step {} is not valid.".format(repr(self.step)) 20 | 21 | 22 | class InvalidDeleteException(InvalidChangeException): 23 | def __init__(self, step: str): 24 | self.step: str = step 25 | 26 | def __str__(self): 27 | return "Deleting the step {} is not valid.".format(repr(self.step)) 28 | 29 | 30 | class InvalidFileException(Exception): 31 | def __init__(self, file: str): 32 | self.file: str = file 33 | 34 | def __str__(self): 35 | return "The file {} is not valid.".format(self.file) 36 | 37 | 38 | class NotationNotFoundException(Exception): 39 | def __init__(self, notation: str): 40 | self.notation: str = notation 41 | 42 | def __str__(self): 43 | return 'Notation "{}" not found in context.'.format(self.notation) 44 | -------------------------------------------------------------------------------- /coqpyt/coq/lsp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sr-lab/coqpyt/75a3cca35f6d0f4043b94c26948afde8869ebd77/coqpyt/coq/lsp/__init__.py -------------------------------------------------------------------------------- /coqpyt/coq/lsp/client.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import threading 3 | import subprocess 4 | 5 | from coqpyt.lsp.structs import * 6 | from coqpyt.lsp.json_rpc_endpoint import JsonRpcEndpoint 7 | from coqpyt.lsp.endpoint import LspEndpoint 8 | from coqpyt.lsp.client import LspClient 9 | from coqpyt.coq.lsp.structs import * 10 | 11 | 12 | class CoqLspClient(LspClient): 13 | """Abstraction to interact with coq-lsp 14 | 15 | Attributes: 16 | file_progress (Dict[str, List[CoqFileProgressParams]]): Contains all 17 | the `$/coq/fileProgress` notifications sent by the server. The 18 | keys are the URIs of the files and the values are the list of 19 | notifications. 20 | """ 21 | 22 | __DEFAULT_INIT_OPTIONS = { 23 | "max_errors": 120000000, 24 | "goal_after_tactic": False, 25 | "show_coq_info_messages": True, 26 | } 27 | 28 | def __init__( 29 | self, 30 | root_uri: str, 31 | timeout: int = 30, 32 | memory_limit: int = 2097152, 33 | coq_lsp: str = "coq-lsp", 34 | coq_lsp_options: Tuple[str] = None, 35 | init_options: Dict = __DEFAULT_INIT_OPTIONS, 36 | ): 37 | """Creates a CoqLspClient 38 | 39 | Args: 40 | root_uri (str): URI to the workspace where coq-lsp will run 41 | The URI can be either a file or a folder. 42 | timeout (int, optional): Timeout used for the coq-lsp operations. 43 | Defaults to 2. 44 | memory_limit (int, optional): RAM limit for the coq-lsp process 45 | in kbytes. It only works for Linux systems. Defaults to 2097152. 46 | coq_lsp(str, optional): Path to the coq-lsp binary. Defaults to "coq-lsp". 47 | init_options (Dict, optional): Initialization options for coq-lsp server. 48 | Available options are: 49 | max_errors (int): Maximum number of errors per file, after that, 50 | coq-lsp will stop checking the file. Defaults to 120000000. 51 | show_coq_info_messages (bool): Show Coq's info messages as diagnostics. 52 | Defaults to false. 53 | show_notices_as_diagnostics (bool): Show Coq's notice messages 54 | as diagnostics, such as `About` and `Search` operations. 55 | Defaults to false. 56 | debug (bool): Enable Debug in Coq Server. Defaults to false. 57 | pp_type (int): Method to print Coq Terms. 58 | 0 = Print to string 59 | 1 = Use jsCoq's Pp rich layout printer 60 | 2 = Coq Layout Engine 61 | Defaults to 1. 62 | """ 63 | self.file_progress: Dict[str, List[CoqFileProgressParams]] = {} 64 | 65 | if sys.platform.startswith("linux"): 66 | command = f"ulimit -v {memory_limit}; {coq_lsp}" 67 | else: 68 | command = f"{coq_lsp}" 69 | 70 | if coq_lsp_options is None: 71 | command += " -D 0" 72 | else: 73 | hasDOption = False 74 | for option in coq_lsp_options: 75 | if option.startswith("-D"): 76 | hasDOption = True 77 | break 78 | if not hasDOption: 79 | command += " -D 0" 80 | command += " " + " ".join(coq_lsp_options) 81 | 82 | proc = subprocess.Popen( 83 | command, 84 | stdout=subprocess.PIPE, 85 | stdin=subprocess.PIPE, 86 | shell=True, 87 | ) 88 | json_rpc_endpoint = JsonRpcEndpoint(proc.stdin, proc.stdout) 89 | lsp_endpoint = LspEndpoint(json_rpc_endpoint, timeout=timeout) 90 | lsp_endpoint.notify_callbacks = { 91 | "$/coq/fileProgress": self.__handle_file_progress, 92 | "textDocument/publishDiagnostics": self.__handle_publish_diagnostics, 93 | } 94 | super().__init__(lsp_endpoint) 95 | workspaces = [{"name": "coq-lsp", "uri": root_uri}] 96 | # This is required to be False since we use it to know if operations 97 | # such as didOpen and didChange already finished. 98 | init_options["eager_diagnostics"] = False 99 | self.initialize( 100 | proc.pid, 101 | "", 102 | root_uri, 103 | init_options, 104 | {}, 105 | "off", 106 | workspaces, 107 | ) 108 | self.initialized() 109 | # Used to check if didOpen and didChange already finished 110 | self.__completed_operation = threading.Event() 111 | 112 | def __handle_publish_diagnostics(self, params: Dict): 113 | self.__completed_operation.set() 114 | 115 | def __handle_file_progress(self, params: Dict): 116 | coqFileProgressKind = CoqFileProgressParams.parse(params) 117 | uri = coqFileProgressKind.textDocument.uri 118 | if uri not in self.file_progress: 119 | self.file_progress[uri] = [coqFileProgressKind] 120 | else: 121 | self.file_progress[uri].append(coqFileProgressKind) 122 | 123 | def __wait_for_operation(self): 124 | timeout = not self.__completed_operation.wait(self.lsp_endpoint.timeout) 125 | self.__completed_operation.clear() 126 | if self.lsp_endpoint.shutdown_flag: 127 | raise ResponseError(ErrorCodes.ServerQuit, "Server quit") 128 | if timeout: 129 | self.shutdown() 130 | self.exit() 131 | raise ResponseError(ErrorCodes.ServerTimeout, "Server timeout") 132 | 133 | def didOpen(self, textDocument: TextDocumentItem): 134 | """Open a text document in the server. 135 | 136 | Args: 137 | textDocument (TextDocumentItem): Text document to open 138 | """ 139 | self.lsp_endpoint.diagnostics[textDocument.uri] = [] 140 | super().didOpen(textDocument) 141 | self.__wait_for_operation() 142 | 143 | def didChange( 144 | self, 145 | textDocument: VersionedTextDocumentIdentifier, 146 | contentChanges: list[TextDocumentContentChangeEvent], 147 | ): 148 | """Submit changes on a text document already open on the server. 149 | 150 | Args: 151 | textDocument (VersionedTextDocumentIdentifier): Text document changed. 152 | contentChanges (list[TextDocumentContentChangeEvent]): Changes made. 153 | """ 154 | self.lsp_endpoint.diagnostics[textDocument.uri] = [] 155 | super().didChange(textDocument, contentChanges) 156 | self.__wait_for_operation() 157 | 158 | def proof_goals( 159 | self, textDocument: TextDocumentIdentifier, position: Position 160 | ) -> Optional[GoalAnswer]: 161 | """Get proof goals and relevant information at a position. 162 | 163 | Args: 164 | textDocument (TextDocumentIdentifier): Text document to consider. 165 | position (Position): Position used to get the proof goals. 166 | 167 | Returns: 168 | GoalAnswer: Contains the goals at a position, messages associated 169 | to the position and if errors exist, the top error at the position. 170 | """ 171 | result_dict = self.lsp_endpoint.call_method( 172 | "proof/goals", textDocument=textDocument, position=position 173 | ) 174 | return GoalAnswer.parse(result_dict) 175 | 176 | def get_document( 177 | self, textDocument: TextDocumentIdentifier 178 | ) -> Optional[FlecheDocument]: 179 | """Get the AST of a text document. 180 | 181 | Args: 182 | textDocument (TextDocumentIdentifier): Text document 183 | 184 | Returns: 185 | Optional[FlecheDocument]: Serialized version of Fleche's document 186 | """ 187 | result_dict = self.lsp_endpoint.call_method( 188 | "coq/getDocument", textDocument=textDocument 189 | ) 190 | return FlecheDocument.parse(result_dict) 191 | 192 | def save_vo(self, textDocument: TextDocumentIdentifier): 193 | """Save a compiled file to disk. 194 | 195 | Args: 196 | textDocument (TextDocumentIdentifier): File to be saved. 197 | The uri in the textDocument should contain an absolute path. 198 | """ 199 | self.lsp_endpoint.call_method("coq/saveVo", textDocument=textDocument) 200 | 201 | # TODO: handle performance data notification? 202 | -------------------------------------------------------------------------------- /coqpyt/coq/lsp/structs.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any, Optional, Tuple, List, Dict 3 | 4 | from coqpyt.lsp.structs import Range, VersionedTextDocumentIdentifier, Position 5 | 6 | 7 | class Hyp(object): 8 | def __init__(self, names: List[str], ty: str, definition: Optional[str] = None): 9 | self.names = names 10 | self.ty = ty 11 | self.definition = definition 12 | 13 | def __repr__(self) -> str: 14 | return ", ".join(self.names) + f": {self.ty}" 15 | 16 | 17 | class Goal(object): 18 | def __init__(self, hyps: List[Hyp], ty: str): 19 | self.hyps = hyps 20 | self.ty = ty 21 | 22 | @staticmethod 23 | def parse(goal: Dict) -> Optional["Goal"]: 24 | if "hyps" not in goal: 25 | return None 26 | for hyp in goal["hyps"]: 27 | if "def" in hyp: 28 | hyp["definition"] = hyp["def"] 29 | hyp.pop("def") 30 | hyps = [Hyp(**hyp) for hyp in goal["hyps"]] 31 | ty = None if "ty" not in goal else goal["ty"] 32 | return Goal(hyps, ty) 33 | 34 | def __repr__(self) -> str: 35 | hyps = list(map(lambda hyp: repr(hyp), self.hyps)) 36 | if len(hyps) > 0: 37 | return "\n".join(hyps) + f"\n\n{self.ty}" 38 | else: 39 | return self.ty 40 | 41 | 42 | class GoalConfig(object): 43 | def __init__( 44 | self, 45 | goals: List[Goal], 46 | stack: List[Tuple[List[Goal], List[Goal]]], 47 | shelf: List[Goal], 48 | given_up: List[Goal], 49 | bullet: Any = None, 50 | ): 51 | self.goals = goals 52 | self.stack = stack 53 | self.shelf = shelf 54 | self.given_up = given_up 55 | self.bullet = bullet 56 | 57 | def __repr__(self) -> str: 58 | bold = lambda text: "\033[1m\033[93m" + text + "\033[0m" 59 | if len(self.goals) > 0: 60 | res = bold("Goals:\n") 61 | for goal in self.goals: 62 | res += "\n" + "-" * 50 + "\n" + repr(goal) + "\n" + "-" * 50 63 | else: 64 | res = "No more goals." 65 | 66 | if any(map(lambda stack: len(stack[0]) > 0 or len(stack[1]) > 0, self.stack)): 67 | res += bold("\n\nStack:") 68 | for stack in self.stack: 69 | for goal in stack[0] + stack[1]: 70 | res += "\n" + "-" * 50 + "\n" + repr(goal) + "\n" + "-" * 50 71 | 72 | if len(self.shelf) > 0: 73 | res += bold("\n\nShelf:") 74 | for goal in self.shelf: 75 | res += "\n" + "-" * 50 + "\n" + repr(goal) + "\n" + "-" * 50 76 | 77 | if len(self.given_up) > 0: 78 | res += bold("\n\nGiven up:") 79 | for goal in self.given_up: 80 | res += "\n" + "-" * 50 + "\n" + repr(goal) + "\n" + "-" * 50 81 | 82 | res += bold("\n\nBullet: ") + repr(self.bullet) 83 | return res 84 | 85 | @staticmethod 86 | def parse(goal_config: Dict) -> Optional["GoalConfig"]: 87 | parse_goals = lambda goals: [Goal.parse(goal) for goal in goals] 88 | goals = parse_goals(goal_config["goals"]) 89 | stack = [(parse_goals(t[0]), parse_goals(t[1])) for t in goal_config["stack"]] 90 | bullet = None if "bullet" not in goal_config else goal_config["bullet"] 91 | shelf = parse_goals(goal_config["shelf"]) 92 | given_up = parse_goals(goal_config["given_up"]) 93 | return GoalConfig(goals, stack, shelf, given_up, bullet=bullet) 94 | 95 | 96 | class Message(object): 97 | def __init__(self, level, text, range=None): 98 | self.level = level 99 | self.text = text 100 | self.range = range 101 | 102 | 103 | class GoalAnswer(object): 104 | def __init__( 105 | self, 106 | textDocument: VersionedTextDocumentIdentifier, 107 | position: Position, 108 | messages: List[Message], 109 | goals: Optional[GoalConfig] = None, 110 | error: Any = None, 111 | program: List = [], 112 | ): 113 | self.textDocument = textDocument 114 | self.position = position 115 | self.messages = messages 116 | self.goals = goals 117 | self.error = error 118 | self.program = program 119 | 120 | def __repr__(self): 121 | res = "\n" 122 | 123 | if len(self.messages) > 0: 124 | res += "Messages:\n" 125 | for message in self.messages: 126 | res += f"{message.level}: {message.text}\n" 127 | 128 | if self.goals is not None: 129 | res += repr(self.goals) 130 | 131 | if self.error is not None: 132 | res += "\nError: " + repr(self.error) 133 | 134 | return res 135 | 136 | @staticmethod 137 | def parse(goal_answer) -> Optional["GoalAnswer"]: 138 | goal_answer["textDocument"] = VersionedTextDocumentIdentifier( 139 | **goal_answer["textDocument"] 140 | ) 141 | goal_answer["position"] = Position( 142 | goal_answer["position"]["line"], goal_answer["position"]["character"] 143 | ) 144 | 145 | if "goals" in goal_answer: 146 | goal_answer["goals"] = GoalConfig.parse(goal_answer["goals"]) 147 | 148 | for i, message in enumerate(goal_answer["messages"]): 149 | if not isinstance(message, str): 150 | if message["range"]: 151 | message["range"] = Range(**message["range"]) 152 | goal_answer["messages"][i] = Message(**message) 153 | 154 | return GoalAnswer(**goal_answer) 155 | 156 | 157 | class Result(object): 158 | def __init__(self, range, message): 159 | self.range = range 160 | self.message = message 161 | 162 | 163 | class Query(object): 164 | def __init__(self, query, results): 165 | self.query = query 166 | self.results = results 167 | 168 | 169 | class RangedSpan(object): 170 | def __init__(self, range: Range, span: Any): 171 | self.range = range 172 | self.span = span 173 | 174 | 175 | class CompletionStatus(object): 176 | def __init__(self, status: str, range: Range): 177 | self.status = status 178 | self.range = range 179 | 180 | 181 | class FlecheDocument(object): 182 | def __init__(self, spans: List[RangedSpan], completed: CompletionStatus): 183 | self.spans = spans 184 | self.completed = completed 185 | 186 | @staticmethod 187 | def parse(fleche_document: Dict) -> Optional["FlecheDocument"]: 188 | if "spans" not in fleche_document or "completed" not in fleche_document: 189 | return None 190 | spans: List[RangedSpan] = [] 191 | for span in fleche_document["spans"]: 192 | range = Range(**span["range"]) 193 | spans.append( 194 | RangedSpan(range, None if "span" not in span else span["span"]) 195 | ) 196 | completion_status = CompletionStatus( 197 | fleche_document["completed"]["status"], 198 | Range(**fleche_document["completed"]["range"]), 199 | ) 200 | return FlecheDocument(spans, completion_status) 201 | 202 | 203 | class CoqFileProgressKind(Enum): 204 | Processing = 1 205 | FatalError = 2 206 | 207 | 208 | class CoqFileProgressProcessingInfo(object): 209 | def __init__(self, range: Range, kind: Optional[CoqFileProgressKind]): 210 | self.range = range 211 | self.kind = kind 212 | 213 | 214 | class CoqFileProgressParams(object): 215 | def __init__( 216 | self, 217 | textDocument: VersionedTextDocumentIdentifier, 218 | processing: List[CoqFileProgressProcessingInfo], 219 | ): 220 | self.textDocument = textDocument 221 | self.processing = processing 222 | 223 | @staticmethod 224 | def parse(coqFileProgressParams: Dict) -> Optional["CoqFileProgressParams"]: 225 | if ( 226 | "textDocument" not in coqFileProgressParams 227 | or "processing" not in coqFileProgressParams 228 | ): 229 | return None 230 | textDocument = VersionedTextDocumentIdentifier( 231 | coqFileProgressParams["textDocument"]["uri"], 232 | coqFileProgressParams["textDocument"]["version"], 233 | ) 234 | processing = [] 235 | for progress in coqFileProgressParams["processing"]: 236 | processing.append( 237 | CoqFileProgressProcessingInfo( 238 | Range(**progress["range"]), 239 | ( 240 | None 241 | if "kind" not in progress 242 | else CoqFileProgressKind(progress["kind"]) 243 | ), 244 | ) 245 | ) 246 | return CoqFileProgressParams(textDocument, processing) 247 | -------------------------------------------------------------------------------- /coqpyt/coq/structs.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any, Optional, List, Union, Callable 3 | 4 | from coqpyt.lsp.structs import Diagnostic, Position 5 | from coqpyt.coq.lsp.structs import RangedSpan, GoalAnswer 6 | 7 | 8 | class SegmentType(Enum): 9 | MODULE = 1 10 | MODULE_TYPE = 2 11 | SECTION = 3 12 | 13 | 14 | class TermType(Enum): 15 | THEOREM = 1 16 | LEMMA = 2 17 | DEFINITION = 3 18 | NOTATION = 4 19 | INDUCTIVE = 5 20 | COINDUCTIVE = 6 21 | RECORD = 7 22 | CLASS = 8 23 | INSTANCE = 9 24 | FIXPOINT = 10 25 | COFIXPOINT = 11 26 | SCHEME = 12 27 | VARIANT = 13 28 | FACT = 14 29 | REMARK = 15 30 | COROLLARY = 16 31 | PROPOSITION = 17 32 | PROPERTY = 18 33 | OBLIGATION = 19 34 | TACTIC = 20 35 | RELATION = 21 36 | SETOID = 22 37 | FUNCTION = 23 38 | DERIVE = 24 39 | EQUATION = 25 40 | OTHER = 100 41 | 42 | 43 | class SegmentStack: 44 | def __init__(self): 45 | self.modules: List[str] = [] 46 | self.module_types: List[str] = [] 47 | self.sections: List[str] = [] 48 | self.stack: List[SegmentType] = [] 49 | self.__current = -1 50 | 51 | def __match_apply(self, type: SegmentType, operation: Callable, *args: Any): 52 | match type: 53 | case SegmentType.MODULE: 54 | operation(self.modules, *args) 55 | case SegmentType.MODULE_TYPE: 56 | operation(self.module_types, *args) 57 | case SegmentType.SECTION: 58 | operation(self.sections, *args) 59 | 60 | def push(self, name: str, type: SegmentType): 61 | self.__current += 1 62 | self.stack.insert(self.__current, type) 63 | self.__match_apply(type, list.append, name) 64 | 65 | def pop(self): 66 | self.__match_apply(self.stack.pop(self.__current), list.pop) 67 | self.__current -= 1 68 | 69 | def go_forward(self, name: str): 70 | self.__current += 1 71 | self.__match_apply(self.stack[self.__current], list.append, name) 72 | 73 | def go_back(self): 74 | self.__match_apply(self.stack[self.__current], list.pop) 75 | self.__current -= 1 76 | 77 | 78 | class Step(object): 79 | def __init__(self, text: str, short_text: str, ast: RangedSpan): 80 | self.text = text 81 | self.short_text = short_text 82 | self.ast = ast 83 | self.diagnostics: List[Diagnostic] = [] 84 | 85 | def __repr__(self) -> str: 86 | return self.text 87 | 88 | 89 | class Term: 90 | def __init__( 91 | self, 92 | step: Step, 93 | type: TermType, 94 | file_path: str, 95 | module: List[str], 96 | ): 97 | """Term of a Coq file. 98 | 99 | Args: 100 | text (str): The textual representation of the term. 101 | ast (RangedSpan): The ast representation of the term. 102 | term_type (TermType): The type of the term. 103 | file_path (str): The file where the term is. 104 | module (str): The module where the term is. 105 | """ 106 | self.step = step 107 | self.type = type 108 | self.file_path = file_path 109 | self.module = module 110 | 111 | def __eq__(self, __value: object) -> bool: 112 | if not isinstance(__value, Term): 113 | return False 114 | return __value.text == self.text 115 | 116 | def __hash__(self) -> int: 117 | return hash(self.text) 118 | 119 | def __repr__(self) -> str: 120 | return self.text 121 | 122 | @property 123 | def text(self) -> str: 124 | return self.step.short_text 125 | 126 | @property 127 | def ast(self) -> RangedSpan: 128 | return self.step.ast 129 | 130 | 131 | class ProofStep: 132 | def __init__( 133 | self, 134 | step: Step, 135 | goals: Union[GoalAnswer, Callable[[Position], GoalAnswer]], 136 | context: List[Term], 137 | ): 138 | self.step = step 139 | self._goals = goals 140 | self.context = context 141 | 142 | def __repr__(self) -> str: 143 | return repr(self.step) 144 | 145 | @property 146 | def goals(self) -> GoalAnswer: 147 | if callable(self._goals): 148 | self._goals = self._goals(self.ast.range.start) 149 | return self._goals 150 | 151 | @goals.setter 152 | def goals(self, goals: Union[GoalAnswer, Callable[[Position], GoalAnswer]]): 153 | self._goals = goals 154 | 155 | @property 156 | def ast(self) -> RangedSpan: 157 | return self.step.ast 158 | 159 | @property 160 | def text(self) -> str: 161 | return self.step.text 162 | 163 | @property 164 | def diagnostics(self) -> List[Diagnostic]: 165 | return self.step.diagnostics 166 | 167 | 168 | class ProofTerm(Term): 169 | def __init__( 170 | self, 171 | term: Term, 172 | context: List[Term], 173 | steps: List[ProofStep], 174 | program: Optional[Term] = None, 175 | ): 176 | super().__init__(term.step, term.type, term.file_path, term.module) 177 | self.steps = steps 178 | self.context = context 179 | self.program = program 180 | -------------------------------------------------------------------------------- /coqpyt/lsp/__init__.py: -------------------------------------------------------------------------------- 1 | # from __future__ import absolute_import 2 | 3 | __all__ = [] 4 | 5 | from coqpyt.lsp.json_rpc_endpoint import JsonRpcEndpoint 6 | from coqpyt.lsp.client import LspClient 7 | from coqpyt.lsp.endpoint import LspEndpoint 8 | from coqpyt.lsp import structs 9 | -------------------------------------------------------------------------------- /coqpyt/lsp/client.py: -------------------------------------------------------------------------------- 1 | from coqpyt.lsp import structs 2 | from coqpyt.lsp.endpoint import LspEndpoint 3 | 4 | 5 | class LspClient(object): 6 | def __init__(self, lsp_endpoint: LspEndpoint): 7 | """ 8 | Constructs a new LspClient instance. 9 | 10 | :param lsp_endpoint: TODO 11 | """ 12 | self.lsp_endpoint = lsp_endpoint 13 | 14 | def initialize( 15 | self, 16 | processId, 17 | rootPath, 18 | rootUri, 19 | initializationOptions, 20 | capabilities, 21 | trace, 22 | workspaceFolders, 23 | ): 24 | """ 25 | The initialize request is sent as the first request from the client to the server. If the server receives a request or notification 26 | before the initialize request it should act as follows: 27 | 28 | 1. For a request the response should be an error with code: -32002. The message can be picked by the server. 29 | 2. Notifications should be dropped, except for the exit notification. This will allow the exit of a server without an initialize request. 30 | 31 | Until the server has responded to the initialize request with an InitializeResult, the client must not send any additional requests or 32 | notifications to the server. In addition the server is not allowed to send any requests or notifications to the client until it has responded 33 | with an InitializeResult, with the exception that during the initialize request the server is allowed to send the notifications window/showMessage, 34 | window/logMessage and telemetry/event as well as the window/showMessageRequest request to the client. 35 | 36 | The initialize request may only be sent once. 37 | 38 | :param int processId: The process Id of the parent process that started the server. Is null if the process has not been started by another process. 39 | If the parent process is not alive then the server should exit (see exit notification) its process. 40 | :param str rootPath: The rootPath of the workspace. Is null if no folder is open. Deprecated in favour of rootUri. 41 | :param DocumentUri rootUri: The rootUri of the workspace. Is null if no folder is open. If both `rootPath` and `rootUri` are set 42 | `rootUri` wins. 43 | :param any initializationOptions: User provided initialization options. 44 | :param ClientCapabilities capabilities: The capabilities provided by the client (editor or tool). 45 | :param Trace trace: The initial trace setting. If omitted trace is disabled ('off'). 46 | :param list workspaceFolders: The workspace folders configured in the client when the server starts. This property is only available if the client supports workspace folders. 47 | It can be `null` if the client supports workspace folders but none are configured. 48 | """ 49 | self.lsp_endpoint.start() 50 | return self.lsp_endpoint.call_method( 51 | "initialize", 52 | processId=processId, 53 | rootPath=rootPath, 54 | rootUri=rootUri, 55 | initializationOptions=initializationOptions, 56 | capabilities=capabilities, 57 | trace=trace, 58 | workspaceFolders=workspaceFolders, 59 | ) 60 | 61 | def initialized(self): 62 | """ 63 | The initialized notification is sent from the client to the server after the client received the result of the initialize request 64 | but before the client is sending any other request or notification to the server. The server can use the initialized notification 65 | for example to dynamically register capabilities. The initialized notification may only be sent once. 66 | """ 67 | self.lsp_endpoint.send_notification("initialized") 68 | 69 | def shutdown(self): 70 | """ 71 | The initialized notification is sent from the client to the server after the client received the result of the initialize request 72 | but before the client is sending any other request or notification to the server. The server can use the initialized notification 73 | for example to dynamically register capabilities. The initialized notification may only be sent once. 74 | """ 75 | self.lsp_endpoint.stop() 76 | return self.lsp_endpoint.call_method("shutdown") 77 | 78 | def exit(self): 79 | """ 80 | The initialized notification is sent from the client to the server after the client received the result of the initialize request 81 | but before the client is sending any other request or notification to the server. The server can use the initialized notification 82 | for example to dynamically register capabilities. The initialized notification may only be sent once. 83 | """ 84 | self.lsp_endpoint.send_notification("exit") 85 | 86 | def didClose(self, textDocument: structs.TextDocumentIdentifier): 87 | return self.lsp_endpoint.send_notification( 88 | "textDocument/didClose", textDocument=textDocument 89 | ) 90 | 91 | def didOpen(self, textDocument): 92 | """ 93 | The document open notification is sent from the client to the server to signal newly opened text documents. The document's truth is 94 | now managed by the client and the server must not try to read the document's truth using the document's uri. Open in this sense 95 | means it is managed by the client. It doesn't necessarily mean that its content is presented in an editor. An open notification must 96 | not be sent more than once without a corresponding close notification send before. This means open and close notification must be 97 | balanced and the max open count for a particular textDocument is one. Note that a server's ability to fulfill requests is independent 98 | of whether a text document is open or closed. 99 | 100 | The DidOpenTextDocumentParams contain the language id the document is associated with. If the language Id of a document changes, the 101 | client needs to send a textDocument/didClose to the server followed by a textDocument/didOpen with the new language id if the server 102 | handles the new language id as well. 103 | 104 | :param TextDocumentItem textDocument: The document that was opened. 105 | """ 106 | return self.lsp_endpoint.send_notification( 107 | "textDocument/didOpen", textDocument=textDocument 108 | ) 109 | 110 | def didChange(self, textDocument, contentChanges): 111 | """ 112 | The document change notification is sent from the client to the server to signal changes to a text document. 113 | In 2.0 the shape of the params has changed to include proper version numbers and language ids. 114 | 115 | :param VersionedTextDocumentIdentifier textDocument: The initial trace setting. If omitted trace is disabled ('off'). 116 | :param TextDocumentContentChangeEvent[] contentChanges: The actual content changes. The content changes describe single state changes 117 | to the document. So if there are two content changes c1 and c2 for a document in state S then c1 move the document 118 | to S' and c2 to S''. 119 | """ 120 | return self.lsp_endpoint.send_notification( 121 | "textDocument/didChange", 122 | textDocument=textDocument, 123 | contentChanges=contentChanges, 124 | ) 125 | 126 | def documentSymbol(self, textDocument): 127 | """ 128 | The document symbol request is sent from the client to the server to return a flat list of all symbols found in a given text document. 129 | Neither the symbol's location range nor the symbol's container name should be used to infer a hierarchy. 130 | 131 | :param TextDocumentItem textDocument: The text document. 132 | """ 133 | result_dict = self.lsp_endpoint.call_method( 134 | "textDocument/documentSymbol", textDocument=textDocument 135 | ) 136 | return [structs.SymbolInformation(**sym) for sym in result_dict] 137 | 138 | def definition(self, textDocument, position): 139 | """ 140 | The goto definition request is sent from the client to the server to resolve the definition location of a symbol at a given text document position. 141 | 142 | :param TextDocumentItem textDocument: The text document. 143 | :param Position position: The position inside the text document. 144 | """ 145 | result_dict = self.lsp_endpoint.call_method( 146 | "textDocument/definition", textDocument=textDocument, position=position 147 | ) 148 | return [structs.Location(**l) for l in result_dict] 149 | 150 | def typeDefinition(self, textDocument, position): 151 | """ 152 | The goto type definition request is sent from the client to the server to resolve the type definition location of a symbol at a given text document position. 153 | 154 | :param TextDocumentItem textDocument: The text document. 155 | :param Position position: The position inside the text document. 156 | """ 157 | result_dict = self.lsp_endpoint.call_method( 158 | "textDocument/definition", textDocument=textDocument, position=position 159 | ) 160 | return [structs.Location(**l) for l in result_dict] 161 | 162 | def signatureHelp(self, textDocument, position): 163 | """ 164 | The signature help request is sent from the client to the server to request signature information at a given cursor position. 165 | 166 | :param TextDocumentItem textDocument: The text document. 167 | :param Position position: The position inside the text document. 168 | """ 169 | result_dict = self.lsp_endpoint.call_method( 170 | "textDocument/signatureHelp", textDocument=textDocument, position=position 171 | ) 172 | return structs.SignatureHelp(**result_dict) 173 | 174 | def completion(self, textDocument, position, context): 175 | """ 176 | The signature help request is sent from the client to the server to request signature information at a given cursor position. 177 | 178 | :param TextDocumentItem textDocument: The text document. 179 | :param Position position: The position inside the text document. 180 | :param CompletionContext context: The completion context. This is only available if the client specifies 181 | to send this using `ClientCapabilities.textDocument.completion.contextSupport === true` 182 | """ 183 | result_dict = self.lsp_endpoint.call_method( 184 | "textDocument/completion", 185 | textDocument=textDocument, 186 | position=position, 187 | context=context, 188 | ) 189 | if "isIncomplete" in result_dict: 190 | return structs.CompletionList(**result_dict) 191 | 192 | return [structs.CompletionItem(**l) for l in result_dict] 193 | 194 | def declaration(self, textDocument, position): 195 | """ 196 | The go to declaration request is sent from the client to the server to resolve the declaration location of a 197 | symbol at a given text document position. 198 | 199 | The result type LocationLink[] got introduce with version 3.14.0 and depends in the corresponding client 200 | capability `clientCapabilities.textDocument.declaration.linkSupport`. 201 | 202 | :param TextDocumentItem textDocument: The text document. 203 | :param Position position: The position inside the text document. 204 | """ 205 | result_dict = self.lsp_endpoint.call_method( 206 | "textDocument/declaration", textDocument=textDocument, position=position 207 | ) 208 | if "uri" in result_dict: 209 | return structs.Location(**result_dict) 210 | 211 | return [ 212 | structs.Location(**l) if "uri" in l else structs.LinkLocation(**l) 213 | for l in result_dict 214 | ] 215 | 216 | def definition(self, textDocument, position): 217 | """ 218 | The go to definition request is sent from the client to the server to resolve the declaration location of a 219 | symbol at a given text document position. 220 | 221 | The result type LocationLink[] got introduce with version 3.14.0 and depends in the corresponding client 222 | capability `clientCapabilities.textDocument.declaration.linkSupport`. 223 | 224 | :param TextDocumentItem textDocument: The text document. 225 | :param Position position: The position inside the text document. 226 | """ 227 | result_dict = self.lsp_endpoint.call_method( 228 | "textDocument/definition", textDocument=textDocument, position=position 229 | ) 230 | if "uri" in result_dict: 231 | return structs.Location(**result_dict) 232 | 233 | return [ 234 | structs.Location(**l) if "uri" in l else structs.LinkLocation(**l) 235 | for l in result_dict 236 | ] 237 | -------------------------------------------------------------------------------- /coqpyt/lsp/endpoint.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import threading 3 | import logging 4 | from typing import List, Dict 5 | from urllib.parse import unquote 6 | 7 | from coqpyt.lsp import structs 8 | 9 | 10 | class LspEndpoint(threading.Thread): 11 | def __init__( 12 | self, json_rpc_endpoint, method_callbacks={}, notify_callbacks={}, timeout=2 13 | ): 14 | threading.Thread.__init__(self) 15 | self.json_rpc_endpoint = json_rpc_endpoint 16 | self.notify_callbacks = notify_callbacks 17 | self.method_callbacks = method_callbacks 18 | self.event_dict = {} 19 | self.response_dict = {} 20 | self.next_id = 0 21 | self.timeout = timeout 22 | self.shutdown_flag = False 23 | self.diagnostics: Dict[str, List[structs.Diagnostic]] = {} 24 | 25 | def handle_result(self, rpc_id, result, error): 26 | self.response_dict[rpc_id] = (result, error) 27 | cond = self.event_dict[rpc_id] 28 | cond.acquire() 29 | cond.notify() 30 | cond.release() 31 | 32 | def stop(self): 33 | self.shutdown_flag = True 34 | 35 | def run(self): 36 | while not self.shutdown_flag: 37 | try: 38 | jsonrpc_message = self.json_rpc_endpoint.recv_response() 39 | if jsonrpc_message is None: 40 | print("server quit") 41 | self.shutdown_flag = True 42 | break 43 | method = jsonrpc_message.get("method") 44 | result = jsonrpc_message.get("result") 45 | error = jsonrpc_message.get("error") 46 | rpc_id = jsonrpc_message.get("id") 47 | params = jsonrpc_message.get("params") 48 | 49 | if method: 50 | if rpc_id: 51 | # a call for method 52 | if method not in self.method_callbacks: 53 | raise structs.ResponseError( 54 | structs.ErrorCodes.MethodNotFound, 55 | "Method not found: {method}".format(method=method), 56 | ) 57 | result = self.method_callbacks[method](params) 58 | self.send_response(rpc_id, result, None) 59 | else: 60 | # a call for notify 61 | if method == "textDocument/publishDiagnostics": 62 | # Default method 63 | logging.debug("received message:", params) 64 | if "diagnostics" in params: 65 | for diagnostic in params["diagnostics"]: 66 | params["uri"] = unquote(params["uri"]) 67 | if params["uri"] not in self.diagnostics: 68 | self.diagnostics[params["uri"]] = [] 69 | self.diagnostics[params["uri"]].append( 70 | structs.Diagnostic(**diagnostic) 71 | ) 72 | if method in self.notify_callbacks: 73 | self.notify_callbacks[method](params) 74 | else: 75 | self.handle_result(rpc_id, result, error) 76 | except structs.ResponseError as e: 77 | self.send_response(rpc_id, None, e) 78 | 79 | def send_response(self, id, result, error): 80 | message_dict = {} 81 | message_dict["jsonrpc"] = "2.0" 82 | message_dict["id"] = id 83 | if result: 84 | message_dict["result"] = result 85 | if error: 86 | message_dict["error"] = error 87 | self.json_rpc_endpoint.send_request(message_dict) 88 | 89 | def send_message(self, method_name, params, id=None): 90 | message_dict = {} 91 | message_dict["jsonrpc"] = "2.0" 92 | if id is not None: 93 | message_dict["id"] = id 94 | message_dict["method"] = method_name 95 | message_dict["params"] = params 96 | self.json_rpc_endpoint.send_request(message_dict) 97 | 98 | def call_method(self, method_name, **kwargs): 99 | current_id = self.next_id 100 | self.next_id += 1 101 | cond = threading.Condition() 102 | self.event_dict[current_id] = cond 103 | 104 | cond.acquire() 105 | self.send_message(method_name, kwargs, current_id) 106 | if self.shutdown_flag: 107 | cond.release() 108 | return None 109 | if not cond.wait(timeout=self.timeout): 110 | raise TimeoutError() 111 | cond.release() 112 | 113 | self.event_dict.pop(current_id) 114 | result, error = self.response_dict.pop(current_id) 115 | if error: 116 | raise structs.ResponseError( 117 | error.get("code"), error.get("message"), error.get("data") 118 | ) 119 | return result 120 | 121 | def send_notification(self, method_name, **kwargs): 122 | self.send_message(method_name, kwargs) 123 | -------------------------------------------------------------------------------- /coqpyt/lsp/json_rpc_endpoint.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import json 3 | import logging 4 | import threading 5 | 6 | from coqpyt.lsp import structs 7 | 8 | JSON_RPC_REQ_FORMAT = "Content-Length: {json_string_len}\r\n\r\n{json_string}" 9 | LEN_HEADER = "Content-Length: " 10 | TYPE_HEADER = "Content-Type: " 11 | 12 | 13 | # TODO: add content-type 14 | 15 | 16 | class MyEncoder(json.JSONEncoder): 17 | """ 18 | Encodes an object in JSON 19 | """ 20 | 21 | def default(self, o): # pylint: disable=E0202 22 | return o.__dict__ 23 | 24 | 25 | class JsonRpcEndpoint(object): 26 | """ 27 | Thread safe JSON RPC endpoint implementation. Responsible to receive and send JSON RPC messages, as described in the 28 | protocol. More information can be found: https://www.jsonrpc.org/ 29 | """ 30 | 31 | def __init__(self, stdin, stdout): 32 | self.stdin = stdin 33 | self.stdout = stdout 34 | self.read_lock = threading.Lock() 35 | self.write_lock = threading.Lock() 36 | self.message_size = None 37 | 38 | @staticmethod 39 | def __add_header(json_string): 40 | """ 41 | Adds a header for the given json string 42 | 43 | :param str json_string: The string 44 | :return: the string with the header 45 | """ 46 | return JSON_RPC_REQ_FORMAT.format( 47 | json_string_len=len(json_string), json_string=json_string 48 | ) 49 | 50 | def send_request(self, message): 51 | """ 52 | Sends the given message. 53 | 54 | :param dict message: The message to send. 55 | """ 56 | try: 57 | json_string = json.dumps(message, cls=MyEncoder) 58 | jsonrpc_req = self.__add_header(json_string) 59 | with self.write_lock: 60 | self.stdin.write(jsonrpc_req.encode()) 61 | self.stdin.flush() 62 | except BrokenPipeError as e: 63 | logging.error(e) 64 | 65 | def recv_response(self): 66 | """ 67 | Receives a message. 68 | 69 | :return: a message 70 | """ 71 | with self.read_lock: 72 | if self.message_size: 73 | if self.message_size.isdigit(): 74 | self.message_size = int(self.message_size) 75 | else: 76 | raise structs.ResponseError( 77 | structs.ErrorCodes.ParseError, "Bad header: size is not int" 78 | ) 79 | while True: 80 | # read header 81 | line = self.stdout.readline() 82 | if not line: 83 | # server quit 84 | return None 85 | line = line.decode("utf-8") 86 | if not line.endswith("\r\n"): 87 | raise structs.ResponseError( 88 | structs.ErrorCodes.ParseError, "Bad header: missing newline" 89 | ) 90 | # remove the "\r\n" 91 | line = line[:-2] 92 | if line == "": 93 | # done with the headers 94 | break 95 | elif line.startswith(LEN_HEADER): 96 | line = line[len(LEN_HEADER) :] 97 | if not line.isdigit(): 98 | raise structs.ResponseError( 99 | structs.ErrorCodes.ParseError, 100 | "Bad header: size is not int", 101 | ) 102 | self.message_size = int(line) 103 | elif line.startswith(TYPE_HEADER): 104 | # nothing todo with type for now. 105 | pass 106 | else: 107 | line = line.split(LEN_HEADER) 108 | if len(line) == 2: 109 | self.message_size = line[1] 110 | raise structs.ResponseError( 111 | structs.ErrorCodes.ParseError, "Bad header: unknown header" 112 | ) 113 | if not self.message_size: 114 | raise structs.ResponseError( 115 | structs.ErrorCodes.ParseError, "Bad header: missing size" 116 | ) 117 | 118 | jsonrpc_res = self.stdout.read(self.message_size).decode("utf-8") 119 | self.message_size = None 120 | return json.loads(jsonrpc_res) 121 | -------------------------------------------------------------------------------- /coqpyt/lsp/structs.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | 4 | def to_type(o, new_type): 5 | """ 6 | Helper funciton that receives an object or a dict and convert it to a new given type. 7 | 8 | :param object|dict o: The object to convert 9 | :param Type new_type: The type to convert to. 10 | """ 11 | if new_type == type(o): 12 | return o 13 | else: 14 | return new_type(**o) 15 | 16 | 17 | class Position(object): 18 | def __init__(self, line, character, offset=0): 19 | """ 20 | Constructs a new Position instance. 21 | 22 | :param int line: Line position in a document (zero-based). 23 | :param int character: Character offset on a line in a document (zero-based). 24 | """ 25 | self.line = line 26 | self.character = character 27 | self.offset = offset 28 | 29 | def __repr__(self) -> str: 30 | return str( 31 | {"line": self.line, "character": self.character, "offset": self.offset} 32 | ) 33 | 34 | def __eq__(self, __value: object) -> bool: 35 | return isinstance(__value, Position) and (self.line, self.character) == ( 36 | __value.line, 37 | __value.character, 38 | ) 39 | 40 | def __gt__(self, __value: object) -> bool: 41 | if not isinstance(__value, Position): 42 | raise TypeError(f"Invalid type for comparison: {type(__value).__name__}") 43 | return (self.line, self.character) > (__value.line, __value.character) 44 | 45 | def __lt__(self, __value: object) -> bool: 46 | return not self.__eq__(__value) and not self.__gt__(__value) 47 | 48 | def __ne__(self, __value: object) -> bool: 49 | return not self.__eq__(__value) 50 | 51 | def __ge__(self, __value: object) -> bool: 52 | return not self.__lt__(__value) 53 | 54 | def __le__(self, __value: object) -> bool: 55 | return not self.__gt__(__value) 56 | 57 | 58 | class Range(object): 59 | def __init__(self, start, end): 60 | """ 61 | Constructs a new Range instance. 62 | 63 | :param Position start: The range's start position. 64 | :param Position end: The range's end position. 65 | """ 66 | self.start = to_type(start, Position) 67 | self.end = to_type(end, Position) 68 | 69 | def __repr__(self) -> str: 70 | return str({"start": repr(self.start), "end": repr(self.end)}) 71 | 72 | def __eq__(self, __value: object) -> bool: 73 | if not isinstance(__value, Range): 74 | raise TypeError(f"Invalid type for comparison: {type(__value).__name__}") 75 | return self.start == __value.start and self.end == __value.end 76 | 77 | def __gt__(self, __value: object) -> bool: 78 | if not isinstance(__value, Range): 79 | raise TypeError(f"Invalid type for comparison: {type(__value).__name__}") 80 | return self.start >= __value.end 81 | 82 | def __lt__(self, __value: object) -> bool: 83 | if not isinstance(__value, Range): 84 | raise TypeError(f"Invalid type for comparison: {type(__value).__name__}") 85 | return self.end <= __value.start 86 | 87 | def __le__(self, __value: object) -> bool: 88 | return self.__lt__(__value) or self.__eq__(__value) 89 | 90 | def __ge__(self, __value: object) -> bool: 91 | return self.__gt__(__value) or self.__eq__(__value) 92 | 93 | 94 | class Location(object): 95 | """ 96 | Represents a location inside a resource, such as a line inside a text file. 97 | """ 98 | 99 | def __init__(self, uri, range): 100 | """ 101 | Constructs a new Location instance. 102 | 103 | :param str uri: Resource file. 104 | :param Range range: The range inside the file 105 | """ 106 | self.uri = uri 107 | self.range = to_type(range, Range) 108 | 109 | 110 | class LocationLink(object): 111 | """ 112 | Represents a link between a source and a target location. 113 | """ 114 | 115 | def __init__( 116 | self, originSelectionRange, targetUri, targetRange, targetSelectionRange 117 | ): 118 | """ 119 | Constructs a new LocationLink instance. 120 | 121 | :param Range originSelectionRange: Span of the origin of this link. 122 | Used as the underlined span for mouse interaction. Defaults to the word range at the mouse position. 123 | :param str targetUri: The target resource identifier of this link. 124 | :param Range targetRange: The full target range of this link. If the target for example is a symbol then target 125 | range is the range enclosing this symbol not including leading/trailing whitespace but everything else 126 | like comments. This information is typically used to highlight the range in the editor. 127 | :param Range targetSelectionRange: The range that should be selected and revealed when this link is being followed, 128 | e.g the name of a function. Must be contained by the the `targetRange`. See also `DocumentSymbol#range` 129 | """ 130 | self.originSelectionRange = to_type(originSelectionRange, Range) 131 | self.targetUri = targetUri 132 | self.targetRange = to_type(targetRange, Range) 133 | self.targetSelectionRange = to_type(targetSelectionRange, Range) 134 | 135 | 136 | class Diagnostic(object): 137 | def __init__( 138 | self, 139 | range, 140 | message, 141 | severity=None, 142 | code=None, 143 | codeDescription=None, 144 | source=None, 145 | tags=None, 146 | relatedInformation=None, 147 | data=None, 148 | ): 149 | """ 150 | Constructs a new Diagnostic instance. 151 | :param Range range: The range at which the message applies.Resource file. 152 | :param int severity: The diagnostic's severity. Can be omitted. If omitted it is up to the 153 | client to interpret diagnostics as error, warning, info or hint. 154 | :param str code: The diagnostic's code, which might appear in the user interface. 155 | :param str source: A human-readable string describing the source of this 156 | diagnostic, e.g. 'typescript' or 'super lint'. 157 | :param str message: The diagnostic's message. 158 | :param list relatedInformation: An array of related diagnostic information, e.g. when symbol-names within 159 | a scope collide all definitions can be marked via this property. 160 | """ 161 | self.range: Range = Range(**range) 162 | self.severity = severity 163 | self.code = code 164 | self.source = source 165 | self.message = message 166 | self.relatedInformation = relatedInformation 167 | 168 | 169 | class DiagnosticSeverity(object): 170 | Error = 1 171 | Warning = 2 # TODO: warning is known in python 172 | Information = 3 173 | Hint = 4 174 | 175 | 176 | class DiagnosticRelatedInformation(object): 177 | def __init__(self, location, message): 178 | """ 179 | Constructs a new Diagnostic instance. 180 | :param Location location: The location of this related diagnostic information. 181 | :param str message: The message of this related diagnostic information. 182 | """ 183 | self.location = location 184 | self.message = message 185 | 186 | 187 | class Command(object): 188 | def __init__(self, title, command, arguments): 189 | """ 190 | Constructs a new Diagnostic instance. 191 | :param str title: Title of the command, like `save`. 192 | :param str command: The identifier of the actual command handler. 193 | :param list argusments: Arguments that the command handler should be invoked with. 194 | """ 195 | self.title = title 196 | self.command = command 197 | self.arguments = arguments 198 | 199 | 200 | class TextDocumentItem(object): 201 | """ 202 | An item to transfer a text document from the client to the server. 203 | """ 204 | 205 | def __init__(self, uri, languageId, version, text): 206 | """ 207 | Constructs a new Diagnostic instance. 208 | 209 | :param DocumentUri uri: Title of the command, like `save`. 210 | :param str languageId: The identifier of the actual command handler. 211 | :param int version: Arguments that the command handler should be invoked with. 212 | :param str text: Arguments that the command handler should be invoked with. 213 | """ 214 | self.uri = uri 215 | self.languageId = languageId 216 | self.version = version 217 | self.text = text 218 | 219 | 220 | class TextDocumentIdentifier(object): 221 | """ 222 | Text documents are identified using a URI. On the protocol level, URIs are passed as strings. 223 | """ 224 | 225 | def __init__(self, uri): 226 | """ 227 | Constructs a new TextDocumentIdentifier instance. 228 | 229 | :param DocumentUri uri: The text document's URI. 230 | """ 231 | self.uri = uri 232 | 233 | 234 | class VersionedTextDocumentIdentifier(TextDocumentIdentifier): 235 | """ 236 | An identifier to denote a specific version of a text document. 237 | """ 238 | 239 | def __init__(self, uri, version): 240 | """ 241 | Constructs a new TextDocumentIdentifier instance. 242 | 243 | :param DocumentUri uri: The text document's URI. 244 | :param int version: The version number of this document. If a versioned 245 | text document identifier is sent from the server to the client and 246 | the file is not open in the editor (the server has not received an 247 | open notification before) the server can send `null` to indicate 248 | that the version is known and the content on disk is the truth (as 249 | speced with document content ownership). 250 | The version number of a document will increase after each change, including 251 | undo/redo. The number doesn't need to be consecutive. 252 | """ 253 | super(VersionedTextDocumentIdentifier, self).__init__(uri) 254 | self.version = version 255 | 256 | 257 | class TextDocumentContentChangeEvent(object): 258 | """ 259 | An event describing a change to a text document. If range and rangeLength are omitted 260 | the new text is considered to be the full content of the document. 261 | """ 262 | 263 | def __init__(self, range, rangeLength, text): 264 | """ 265 | Constructs a new TextDocumentContentChangeEvent instance. 266 | 267 | :param Range range: The range of the document that changed. 268 | :param int rangeLength: The length of the range that got replaced. 269 | :param str text: The new text of the range/document. 270 | """ 271 | self.range = range 272 | self.rangeLength = rangeLength 273 | self.text = text 274 | 275 | 276 | class TextDocumentPositionParams(object): 277 | """ 278 | A parameter literal used in requests to pass a text document and a position inside that document. 279 | """ 280 | 281 | def __init__(self, textDocument, position): 282 | """ 283 | Constructs a new TextDocumentPositionParams instance. 284 | 285 | :param TextDocumentIdentifier textDocument: The text document. 286 | :param Position position: The position inside the text document. 287 | """ 288 | self.textDocument = textDocument 289 | self.position = position 290 | 291 | 292 | class LANGUAGE_IDENTIFIER(object): 293 | BAT = "bat" 294 | BIBTEX = "bibtex" 295 | CLOJURE = "clojure" 296 | COFFESCRIPT = "coffeescript" 297 | C = "c" 298 | CPP = "cpp" 299 | CSHARP = "csharp" 300 | CSS = "css" 301 | DIFF = "diff" 302 | DOCKERFILE = "dockerfile" 303 | FSHARP = "fsharp" 304 | GIT_COMMIT = "git-commit" 305 | GIT_REBASE = "git-rebase" 306 | GO = "go" 307 | GROOVY = "groovy" 308 | HANDLEBARS = "handlebars" 309 | HTML = "html" 310 | INI = "ini" 311 | JAVA = "java" 312 | JAVASCRIPT = "javascript" 313 | JSON = "json" 314 | LATEX = "latex" 315 | LESS = "less" 316 | LUA = "lua" 317 | MAKEFILE = "makefile" 318 | MARKDOWN = "markdown" 319 | OBJECTIVE_C = "objective-c" 320 | OBJECTIVE_CPP = "objective-cpp" 321 | Perl = "perl" 322 | PHP = "php" 323 | POWERSHELL = "powershell" 324 | PUG = "jade" 325 | PYTHON = "python" 326 | R = "r" 327 | RAZOR = "razor" 328 | RUBY = "ruby" 329 | RUST = "rust" 330 | SASS = "sass" 331 | SCSS = "scss" 332 | ShaderLab = "shaderlab" 333 | SHELL_SCRIPT = "shellscript" 334 | SQL = "sql" 335 | SWIFT = "swift" 336 | TYPE_SCRIPT = "typescript" 337 | TEX = "tex" 338 | VB = "vb" 339 | XML = "xml" 340 | XSL = "xsl" 341 | YAML = "yaml" 342 | 343 | 344 | class SymbolKind(enum.Enum): 345 | File = 1 346 | Module = 2 347 | Namespace = 3 348 | Package = 4 349 | Class = 5 350 | Method = 6 351 | Property = 7 352 | Field = 8 353 | Constructor = 9 354 | Enum = 10 355 | Interface = 11 356 | Function = 12 357 | Variable = 13 358 | Constant = 14 359 | String = 15 360 | Number = 16 361 | Boolean = 17 362 | Array = 18 363 | Object = 19 364 | Key = 20 365 | Null = 21 366 | EnumMember = 22 367 | Struct = 23 368 | Event = 24 369 | Operator = 25 370 | TypeParameter = 26 371 | 372 | 373 | class SymbolInformation(object): 374 | """ 375 | Represents information about programming constructs like variables, classes, interfaces etc. 376 | """ 377 | 378 | def __init__( 379 | self, 380 | name, 381 | kind, 382 | detail=None, 383 | range=None, 384 | selectionRange=None, 385 | location=None, 386 | containerName=None, 387 | deprecated=False, 388 | ): 389 | """ 390 | Constructs a new SymbolInformation instance. 391 | 392 | :param str name: The name of this symbol. 393 | :param int kind: The kind of this symbol. 394 | :param bool Location: The location of this symbol. The location's range is used by a tool 395 | to reveal the location in the editor. If the symbol is selected in the 396 | tool the range's start information is used to position the cursor. So 397 | the range usually spans more then the actual symbol's name and does 398 | normally include things like visibility modifiers. 399 | 400 | The range doesn't have to denote a node range in the sense of a abstract 401 | syntax tree. It can therefore not be used to re-construct a hierarchy of 402 | the symbols. 403 | :param str containerName: The name of the symbol containing this symbol. This information is for 404 | user interface purposes (e.g. to render a qualifier in the user interface 405 | if necessary). It can't be used to re-infer a hierarchy for the document 406 | symbols. 407 | :param bool deprecated: Indicates if this symbol is deprecated. 408 | """ 409 | self.name = name 410 | self.detail = detail 411 | self.kind = SymbolKind(kind) 412 | self.range = range 413 | self.selectionRange = selectionRange 414 | self.deprecated = deprecated 415 | # self.location = to_type(location, Location) 416 | self.containerName = containerName 417 | 418 | 419 | class ParameterInformation(object): 420 | """ 421 | Represents a parameter of a callable-signature. A parameter can 422 | have a label and a doc-comment. 423 | """ 424 | 425 | def __init__(self, label, documentation=""): 426 | """ 427 | Constructs a new ParameterInformation instance. 428 | 429 | :param str label: The label of this parameter. Will be shown in the UI. 430 | :param str documentation: The human-readable doc-comment of this parameter. Will be shown in the UI but can be omitted. 431 | """ 432 | self.label = label 433 | self.documentation = documentation 434 | 435 | 436 | class SignatureInformation(object): 437 | """ 438 | Represents the signature of something callable. A signature 439 | can have a label, like a function-name, a doc-comment, and 440 | a set of parameters. 441 | """ 442 | 443 | def __init__(self, label, documentation="", parameters=[]): 444 | """ 445 | Constructs a new SignatureInformation instance. 446 | 447 | :param str label: The label of this signature. Will be shown in the UI. 448 | :param str documentation: The human-readable doc-comment of this signature. Will be shown in the UI but can be omitted. 449 | :param ParameterInformation[] parameters: The parameters of this signature. 450 | """ 451 | self.label = label 452 | self.documentation = documentation 453 | self.parameters = [ 454 | to_type(parameter, ParameterInformation) for parameter in parameters 455 | ] 456 | 457 | 458 | class SignatureHelp(object): 459 | """ 460 | Signature help represents the signature of something 461 | callable. There can be multiple signature but only one 462 | active and only one active parameter. 463 | """ 464 | 465 | def __init__(self, signatures, activeSignature=0, activeParameter=0): 466 | """ 467 | Constructs a new SignatureHelp instance. 468 | 469 | :param SignatureInformation[] signatures: One or more signatures. 470 | :param int activeSignature: 471 | :param int activeParameter: 472 | """ 473 | self.signatures = [ 474 | to_type(signature, SignatureInformation) for signature in signatures 475 | ] 476 | self.activeSignature = activeSignature 477 | self.activeParameter = activeParameter 478 | 479 | 480 | class CompletionTriggerKind(object): 481 | Invoked = 1 482 | TriggerCharacter = 2 483 | TriggerForIncompleteCompletions = 3 484 | 485 | 486 | class CompletionContext(object): 487 | """ 488 | Contains additional information about the context in which a completion request is triggered. 489 | """ 490 | 491 | def __init__(self, triggerKind, triggerCharacter=None): 492 | """ 493 | Constructs a new CompletionContext instance. 494 | 495 | :param CompletionTriggerKind triggerKind: How the completion was triggered. 496 | :param str triggerCharacter: The trigger character (a single character) that has trigger code complete. 497 | Is undefined if `triggerKind !== CompletionTriggerKind.TriggerCharacter` 498 | """ 499 | self.triggerKind = triggerKind 500 | if triggerCharacter: 501 | self.triggerCharacter = triggerCharacter 502 | 503 | 504 | class TextEdit(object): 505 | """ 506 | A textual edit applicable to a text document. 507 | """ 508 | 509 | def __init__(self, range, newText): 510 | """ 511 | :param Range range: The range of the text document to be manipulated. To insert 512 | text into a document create a range where start === end. 513 | :param str newText: The string to be inserted. For delete operations use an empty string. 514 | """ 515 | self.range = range 516 | self.newText = newText 517 | 518 | 519 | class InsertTextFormat(object): 520 | PlainText = 1 521 | Snippet = 2 522 | 523 | 524 | class CompletionItem(object): 525 | """ """ 526 | 527 | def __init__( 528 | self, 529 | label, 530 | kind=None, 531 | detail=None, 532 | documentation=None, 533 | deprecated=None, 534 | presented=None, 535 | sortText=None, 536 | filterText=None, 537 | insertText=None, 538 | insertTextFormat=None, 539 | textEdit=None, 540 | additionalTextEdits=None, 541 | commitCharacters=None, 542 | command=None, 543 | data=None, 544 | score=0.0, 545 | ): 546 | """ 547 | :param str label: The label of this completion item. By default also the text that is inserted when selecting 548 | this completion. 549 | :param int kind: The kind of this completion item. Based of the kind an icon is chosen by the editor. 550 | :param str detail: A human-readable string with additional information about this item, like type or symbol information. 551 | :param tr ocumentation: A human-readable string that represents a doc-comment. 552 | :param bool deprecated: Indicates if this item is deprecated. 553 | :param bool presented: Select this item when showing. Note: that only one completion item can be selected and that the 554 | tool / client decides which item that is. The rule is that the first item of those that match best is selected. 555 | :param str sortText: A string that should be used when comparing this item with other items. When `falsy` the label is used. 556 | :param str filterText: A string that should be used when filtering a set of completion items. When `falsy` the label is used. 557 | :param str insertText: A string that should be inserted into a document when selecting this completion. When `falsy` the label is used. 558 | The `insertText` is subject to interpretation by the client side. Some tools might not take the string literally. For example 559 | VS Code when code complete is requested in this example `con` and a completion item with an `insertText` of `console` is provided it 560 | will only insert `sole`. Therefore it is recommended to use `textEdit` instead since it avoids additional client side interpretation. 561 | @deprecated Use textEdit instead. 562 | :param InsertTextFormat insertTextFormat: The format of the insert text. The format applies to both the `insertText` property 563 | and the `newText` property of a provided `textEdit`. 564 | :param TextEdit textEdit: An edit which is applied to a document when selecting this completion. When an edit is provided the value of `insertText` is ignored. 565 | Note:* The range of the edit must be a single line range and it must contain the position at which completion 566 | has been requested. 567 | :param TextEdit additionalTextEdits: An optional array of additional text edits that are applied when selecting this completion. 568 | Edits must not overlap (including the same insert position) with the main edit nor with themselves. 569 | Additional text edits should be used to change text unrelated to the current cursor position 570 | (for example adding an import statement at the top of the file if the completion item will 571 | insert an unqualified type). 572 | :param str commitCharacters: An optional set of characters that when pressed while this completion is active will accept it first and 573 | then type that character. *Note* that all commit characters should have `length=1` and that superfluous 574 | characters will be ignored. 575 | :param Command command: An optional command that is executed *after* inserting this completion. Note: that 576 | additional modifications to the current document should be described with the additionalTextEdits-property. 577 | :param data: An data entry field that is preserved on a completion item between a completion and a completion resolve request. 578 | :param float score: Score of the code completion item. 579 | """ 580 | self.label = label 581 | self.kind = kind 582 | self.detail = detail 583 | self.documentation = documentation 584 | self.deprecated = deprecated 585 | self.presented = presented 586 | self.sortText = sortText 587 | self.filterText = filterText 588 | self.insertText = insertText 589 | self.insertTextFormat = insertTextFormat 590 | self.textEdit = textEdit 591 | self.additionalTextEdits = additionalTextEdits 592 | self.commitCharacters = commitCharacters 593 | self.command = command 594 | self.data = data 595 | self.score = score 596 | 597 | 598 | class CompletionItemKind(enum.Enum): 599 | Text = 1 600 | Method = 2 601 | Function = 3 602 | Constructor = 4 603 | Field = 5 604 | Variable = 6 605 | Class = 7 606 | Interface = 8 607 | Module = 9 608 | Property = 10 609 | Unit = 11 610 | Value = 12 611 | Enum = 13 612 | Keyword = 14 613 | Snippet = 15 614 | Color = 16 615 | File = 17 616 | Reference = 18 617 | Folder = 19 618 | EnumMember = 20 619 | Constant = 21 620 | Struct = 22 621 | Event = 23 622 | Operator = 24 623 | TypeParameter = 25 624 | 625 | 626 | class CompletionList(object): 627 | """ 628 | Represents a collection of [completion items](#CompletionItem) to be presented in the editor. 629 | """ 630 | 631 | def __init__(self, isIncomplete, items): 632 | """ 633 | Constructs a new CompletionContext instance. 634 | 635 | :param bool isIncomplete: This list it not complete. Further typing should result in recomputing this list. 636 | :param CompletionItem items: The completion items. 637 | """ 638 | self.isIncomplete = isIncomplete 639 | self.items = [to_type(i, CompletionItem) for i in items] 640 | 641 | 642 | class ErrorCodes(enum.Enum): 643 | # Defined by JSON RPC 644 | ParseError = -32700 645 | InvalidRequest = -32600 646 | MethodNotFound = -32601 647 | InvalidParams = -32602 648 | InternalError = -32603 649 | serverErrorStart = -32099 650 | serverErrorEnd = -32000 651 | ServerTimeout = -32004 652 | ServerQuit = -32003 653 | ServerNotInitialized = -32002 654 | UnknownErrorCode = -32001 655 | 656 | # Defined by the protocol. 657 | RequestCancelled = -32800 658 | ContentModified = -32801 659 | 660 | 661 | class ResponseError(Exception): 662 | def __init__(self, code, message, data=None): 663 | if isinstance(code, ErrorCodes): 664 | code = code.value 665 | self.code = code 666 | self.message = message 667 | if data: 668 | self.data = data 669 | -------------------------------------------------------------------------------- /coqpyt/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sr-lab/coqpyt/75a3cca35f6d0f4043b94c26948afde8869ebd77/coqpyt/tests/__init__.py -------------------------------------------------------------------------------- /coqpyt/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def pytest_addoption(parser): 5 | parser.addoption( 6 | "--runextra", 7 | action="store_true", 8 | default=False, 9 | help="run extra tests from external libraries", 10 | ) 11 | 12 | 13 | def pytest_configure(config): 14 | config.addinivalue_line("markers", "extra: mark test as from external library") 15 | 16 | 17 | def pytest_collection_modifyitems(config, items): 18 | if config.getoption("--runextra"): 19 | return 20 | skip_extra = pytest.mark.skip(reason="need --runextra option to run") 21 | for item in items: 22 | if "extra" in item.keywords: 23 | item.add_marker(skip_extra) 24 | -------------------------------------------------------------------------------- /coqpyt/tests/proof_file/expected/imports.yml: -------------------------------------------------------------------------------- 1 | proofs: 2 | # 1st proof 3 | - text: "Theorem plus_O_n : forall n:nat, 0 + n = n." 4 | steps: 5 | - text: "\nProof." 6 | goals: 7 | goals: 8 | goals: 9 | - hyps: [] 10 | ty: "∀ n : nat, 0 + n = n" 11 | position: 12 | line: 6 13 | character: 0 14 | - text: "\n intros n." 15 | goals: 16 | goals: 17 | goals: 18 | - hyps: [] 19 | ty: "∀ n : nat, 0 + n = n" 20 | position: 21 | line: 7 22 | character: 2 23 | - text: "\n Print plus." 24 | goals: 25 | goals: 26 | goals: 27 | - hyps: 28 | - names: 29 | - n 30 | ty: nat 31 | ty: "0 + n = n" 32 | position: 33 | line: 8 34 | character: 2 35 | context: 36 | - text: "Notation plus := Nat.add (only parsing)." 37 | type: NOTATION 38 | - text: "\n Print Nat.add." 39 | goals: 40 | goals: 41 | goals: 42 | - hyps: 43 | - names: 44 | - n 45 | ty: nat 46 | ty: "0 + n = n" 47 | position: 48 | line: 9 49 | character: 2 50 | context: 51 | - text: 'Fixpoint add n m := match n with | 0 => m | S p => S (p + m) end where "n + m" := (add n m) : nat_scope.' 52 | type: FIXPOINT 53 | - text: "\n reduce_eq." 54 | goals: 55 | goals: 56 | goals: 57 | - hyps: 58 | - names: 59 | - n 60 | ty: nat 61 | ty: "0 + n = n" 62 | position: 63 | line: 10 64 | character: 2 65 | context: 66 | - text: "Ltac reduce_eq := simpl; reflexivity." 67 | type: TACTIC 68 | - text: "\nQed." 69 | goals: 70 | position: 71 | line: 11 72 | character: 0 73 | context: 74 | - text: "Inductive nat : Set := | O : nat | S : nat -> nat." 75 | type: INDUCTIVE 76 | - text: 'Notation "x = y" := (eq x y) : type_scope.' 77 | type: NOTATION 78 | - text: 'Fixpoint add n m := match n with | 0 => m | S p => S (p + m) end where "n + m" := (add n m) : nat_scope.' 79 | type: NOTATION 80 | # 2nd proof 81 | - text: "Definition mult_0_plus : ∀ n m : nat, 0 + (S n * m) = S n * m." 82 | steps: 83 | - text: "\nProof." 84 | goals: 85 | goals: 86 | goals: 87 | - hyps: [] 88 | ty: "∀ n m : nat, 0 + S n * m = S n * m" 89 | position: 90 | line: 15 91 | character: 0 92 | - text: "\n intros n m." 93 | goals: 94 | goals: 95 | goals: 96 | - hyps: [] 97 | ty: "∀ n m : nat, 0 + S n * m = S n * m" 98 | position: 99 | line: 16 100 | character: 2 101 | - text: "\n rewrite -> (plus_O_n (S n * m))." 102 | goals: 103 | goals: 104 | goals: 105 | - hyps: 106 | - names: 107 | - n 108 | - m 109 | ty: nat 110 | ty: "0 + S n * m = S n * m" 111 | position: 112 | line: 17 113 | character: 2 114 | context: 115 | - text: "Local Theorem plus_O_n : forall n:nat, 0 + n = n." 116 | type: THEOREM 117 | - text: 'Fixpoint mul n m := match n with | 0 => 0 | S p => m + p * m end where "n * m" := (mul n m) : nat_scope.' 118 | type: NOTATION 119 | - text: "Inductive nat : Set := | O : nat | S : nat -> nat." 120 | type: INDUCTIVE 121 | - text: "\n Locate test_import2.plus_O_n." 122 | goals: 123 | goals: 124 | goals: 125 | - hyps: 126 | - names: 127 | - n 128 | - m 129 | ty: nat 130 | ty: "S n * m = S n * m" 131 | position: 132 | line: 18 133 | character: 2 134 | # FIXME in the future we should get a Local Theorem from the other file here 135 | - text: "\n Locate Peano.plus_O_n." 136 | goals: 137 | goals: 138 | goals: 139 | - hyps: 140 | - names: 141 | - n 142 | - m 143 | ty: nat 144 | ty: "S n * m = S n * m" 145 | position: 146 | line: 19 147 | character: 2 148 | context: 149 | - text: "Lemma plus_O_n : forall n:nat, 0 + n = n." 150 | type: LEMMA 151 | - text: "\n reflexivity." 152 | goals: 153 | goals: 154 | goals: 155 | - hyps: 156 | - names: 157 | - n 158 | - m 159 | ty: nat 160 | ty: "S n * m = S n * m" 161 | position: 162 | line: 20 163 | character: 2 164 | - text: "\nDefined." 165 | goals: 166 | position: 167 | line: 21 168 | character: 0 169 | context: 170 | - "8.19.x": 171 | text: "Notation \"∀ x .. y , P\" := (forall x, .. (forall y, P) ..) (at level 10, x binder, y binder, P at level 200, format \"'[ ' '[ ' ∀ x .. y ']' , '/' P ']'\") : type_scope." 172 | type: NOTATION 173 | "8.20.x": 174 | text: "Notation \"∀ x .. y , P\" := (forall x, .. (forall y, P) ..) (at level 10, x binder, y binder, P at level 200, format \"'[ ' '[ ' ∀ x .. y ']' , '/' P ']'\") : type_scope." 175 | type: NOTATION 176 | default: 177 | text: "Notation \"∀ x .. y , P\" := (forall x, .. (forall y, P) ..) (at level 200, x binder, y binder, right associativity, format \"'[ ' '[ ' ∀ x .. y ']' , '/' P ']'\") : type_scope." 178 | type: NOTATION 179 | - text: 'Notation "x = y" := (eq x y) : type_scope.' 180 | type: NOTATION 181 | - text: 'Fixpoint add n m := match n with | 0 => m | S p => S (p + m) end where "n + m" := (add n m) : nat_scope.' 182 | type: NOTATION 183 | - text: 'Fixpoint mul n m := match n with | 0 => 0 | S p => m + p * m end where "n * m" := (mul n m) : nat_scope.' 184 | type: NOTATION 185 | - text: "Inductive nat : Set := | O : nat | S : nat -> nat." 186 | type: INDUCTIVE -------------------------------------------------------------------------------- /coqpyt/tests/proof_file/expected/list_notation.yml: -------------------------------------------------------------------------------- 1 | proofs: 2 | - text: "Goal [1] ++ [2] = [1; 2]." 3 | steps: 4 | - text: "\nProof." 5 | goals: 6 | goals: 7 | goals: 8 | - hyps: [] 9 | ty: "[1] ++ [2] = [1; 2]" 10 | position: 11 | line: 4 12 | character: 0 13 | - text: " reflexivity." 14 | goals: 15 | goals: 16 | goals: 17 | - hyps: [] 18 | ty: "[1] ++ [2] = [1; 2]" 19 | position: 20 | line: 4 21 | character: 7 22 | context: 23 | - text: "Class Reflexive (R : A -> A -> Prop) := reflexivity : forall x : A, R x x." 24 | type: CLASS 25 | - text: " Qed." 26 | goals: 27 | position: 28 | line: 4 29 | character: 20 30 | context: 31 | - text: 'Notation "x = y" := (eq x y) : type_scope.' 32 | type: NOTATION 33 | - text: 'Infix "++" := app (right associativity, at level 60) : list_scope.' 34 | type: NOTATION 35 | - text: 'Notation "[ x ]" := (cons x nil) : list_scope.' 36 | type: NOTATION 37 | module: ["ListNotations"] 38 | - text: "Notation \"[ x ; y ; .. ; z ]\" := (cons x (cons y .. (cons z nil) ..)) (format \"[ '[' x ; '/' y ; '/' .. ; '/' z ']' ]\") : list_scope." 39 | type: NOTATION 40 | module: ["ListNotations"] -------------------------------------------------------------------------------- /coqpyt/tests/proof_file/expected/valid_file.yml: -------------------------------------------------------------------------------- 1 | proofs: 2 | # 1st proof 3 | - text: "Theorem plus_O_n : forall n:nat, 0 + n = n." 4 | steps: 5 | - text: "\n Proof." 6 | goals: 7 | goals: 8 | goals: 9 | - hyps: [] 10 | ty: "∀ n : nat, 0 + n = n" 11 | position: 12 | line: 9 13 | character: 4 14 | - text: "\n intros n." 15 | goals: 16 | goals: 17 | goals: 18 | - hyps: [] 19 | ty: "∀ n : nat, 0 + n = n" 20 | position: 21 | line: 10 22 | character: 6 23 | - text: "\n Print plus." 24 | goals: 25 | goals: 26 | goals: 27 | - hyps: 28 | - names: 29 | - n 30 | ty: nat 31 | ty: "0 + n = n" 32 | position: 33 | line: 11 34 | character: 6 35 | context: 36 | - text: "Notation plus := Nat.add (only parsing)." 37 | type: NOTATION 38 | - text: "\n Print Nat.add." 39 | goals: 40 | goals: 41 | goals: 42 | - hyps: 43 | - names: 44 | - n 45 | ty: nat 46 | ty: "0 + n = n" 47 | position: 48 | line: 12 49 | character: 6 50 | context: 51 | - text: 'Fixpoint add n m := match n with | 0 => m | S p => S (p + m) end where "n + m" := (add n m) : nat_scope.' 52 | type: FIXPOINT 53 | - text: "\n reduce_eq." 54 | goals: 55 | goals: 56 | goals: 57 | - hyps: 58 | - names: 59 | - n 60 | ty: nat 61 | ty: "0 + n = n" 62 | position: 63 | line: 13 64 | character: 6 65 | context: 66 | - text: "Ltac reduce_eq := simpl; reflexivity." 67 | type: TACTIC 68 | - text: "\n Qed." 69 | goals: 70 | position: 71 | line: 14 72 | character: 4 73 | context: 74 | - text: "Inductive nat : Set := | O : nat | S : nat -> nat." 75 | type: INDUCTIVE 76 | - text: 'Notation "x = y" := (eq x y) : type_scope.' 77 | type: NOTATION 78 | - text: 'Fixpoint add n m := match n with | 0 => m | S p => S (p + m) end where "n + m" := (add n m) : nat_scope.' 79 | type: NOTATION 80 | 81 | # 2nd proof 82 | - text: "Definition mult_0_plus : ∀ n m : nat, 0 + (S n * m) = S n * m." 83 | steps: 84 | - text: "\n Proof." 85 | goals: 86 | goals: 87 | goals: 88 | - hyps: [] 89 | ty: "∀ n m : nat, 0 + S n * m = S n * m" 90 | position: 91 | line: 21 92 | character: 2 93 | range: 94 | start: 95 | line: 21 96 | character: 2 97 | end: 98 | line: 21 99 | character: 8 100 | - text: "\n intros n m." 101 | goals: 102 | goals: 103 | goals: 104 | - hyps: [] 105 | ty: "∀ n m : nat, 0 + S n * m = S n * m" 106 | position: 107 | line: 22 108 | character: 4 109 | range: 110 | start: 111 | line: 22 112 | character: 4 113 | end: 114 | line: 22 115 | character: 15 116 | - text: "\n rewrite -> (plus_O_n (S n * m))." 117 | goals: 118 | goals: 119 | goals: 120 | - hyps: 121 | - names: 122 | - n 123 | - m 124 | ty: nat 125 | ty: "0 + S n * m = S n * m" 126 | position: 127 | line: 23 128 | character: 4 129 | range: 130 | start: 131 | line: 23 132 | character: 4 133 | end: 134 | line: 23 135 | character: 36 136 | context: 137 | - text: "Lemma plus_O_n : forall n:nat, 0 + n = n." 138 | type: LEMMA 139 | - text: 'Fixpoint mul n m := match n with | 0 => 0 | S p => m + p * m end where "n * m" := (mul n m) : nat_scope.' 140 | type: NOTATION 141 | - text: "Inductive nat : Set := | O : nat | S : nat -> nat." 142 | type: INDUCTIVE 143 | - text: "\n Compute True /\\ True." 144 | goals: 145 | goals: 146 | goals: 147 | - hyps: 148 | - names: 149 | - n 150 | - m 151 | ty: nat 152 | ty: "S n * m = S n * m" 153 | position: 154 | line: 24 155 | character: 4 156 | range: 157 | start: 158 | line: 24 159 | character: 4 160 | end: 161 | line: 24 162 | character: 25 163 | context: 164 | - text: 'Inductive and (A B:Prop) : Prop := conj : A -> B -> A /\ B where "A /\ B" := (and A B) : type_scope.' 165 | type: NOTATION 166 | - text: "Inductive True : Prop := I : True." 167 | type: INDUCTIVE 168 | - text: "\n reflexivity." 169 | goals: 170 | goals: 171 | goals: 172 | - hyps: 173 | - names: 174 | - n 175 | - m 176 | ty: nat 177 | ty: "S n * m = S n * m" 178 | position: 179 | line: 25 180 | character: 4 181 | range: 182 | start: 183 | line: 25 184 | character: 4 185 | end: 186 | line: 25 187 | character: 16 188 | - text: "\n Abort." 189 | goals: 190 | position: 191 | line: 26 192 | character: 2 193 | range: 194 | start: 195 | line: 26 196 | character: 2 197 | end: 198 | line: 26 199 | character: 8 200 | context: 201 | - "8.19.x": 202 | text: "Notation \"∀ x .. y , P\" := (forall x, .. (forall y, P) ..) (at level 10, x binder, y binder, P at level 200, format \"'[ ' '[ ' ∀ x .. y ']' , '/' P ']'\") : type_scope." 203 | type: NOTATION 204 | "8.20.x": 205 | text: "Notation \"∀ x .. y , P\" := (forall x, .. (forall y, P) ..) (at level 10, x binder, y binder, P at level 200, format \"'[ ' '[ ' ∀ x .. y ']' , '/' P ']'\") : type_scope." 206 | type: NOTATION 207 | default: 208 | text: "Notation \"∀ x .. y , P\" := (forall x, .. (forall y, P) ..) (at level 200, x binder, y binder, right associativity, format \"'[ ' '[ ' ∀ x .. y ']' , '/' P ']'\") : type_scope." 209 | type: NOTATION 210 | - text: 'Notation "x = y" := (eq x y) : type_scope.' 211 | type: NOTATION 212 | - text: 'Fixpoint add n m := match n with | 0 => m | S p => S (p + m) end where "n + m" := (add n m) : nat_scope.' 213 | type: NOTATION 214 | - text: 'Fixpoint mul n m := match n with | 0 => 0 | S p => m + p * m end where "n * m" := (mul n m) : nat_scope.' 215 | type: NOTATION 216 | - text: "Inductive nat : Set := | O : nat | S : nat -> nat." 217 | type: INDUCTIVE 218 | 219 | # 3rd proof 220 | - text: "Theorem plus_O_n : forall n:nat, n = 0 + n." 221 | steps: 222 | - text: "\n intros n." 223 | goals: 224 | goals: 225 | goals: 226 | - hyps: [] 227 | ty: "∀ n : nat, n = 0 + n" 228 | position: 229 | line: 34 230 | character: 6 231 | - text: "\n Compute mk_example n n." 232 | goals: 233 | goals: 234 | goals: 235 | - hyps: 236 | - names: 237 | - n 238 | ty: nat 239 | ty: "n = 0 + n" 240 | position: 241 | line: 35 242 | character: 6 243 | context: 244 | - text: "Record example := mk_example { fst : nat; snd : nat }." 245 | type: RECORD 246 | module: ["Extra", "Fst"] 247 | - text: "\n Compute Out.In.plus_O_n." 248 | goals: 249 | goals: 250 | goals: 251 | - hyps: 252 | - names: 253 | - n 254 | ty: nat 255 | ty: "n = 0 + n" 256 | position: 257 | line: 36 258 | character: 6 259 | context: 260 | - text: "Theorem plus_O_n : forall n:nat, 0 + n = n." 261 | type: THEOREM 262 | module: ["Out", "In"] 263 | - text: "\n reduce_eq." 264 | goals: 265 | goals: 266 | goals: 267 | - hyps: 268 | - names: 269 | - n 270 | ty: nat 271 | ty: "n = 0 + n" 272 | position: 273 | line: 37 274 | character: 6 275 | context: 276 | - text: "Ltac reduce_eq := simpl; reflexivity." 277 | type: TACTIC 278 | - text: "\n Defined." 279 | goals: 280 | position: 281 | line: 38 282 | character: 4 283 | context: 284 | - text: "Inductive nat : Set := | O : nat | S : nat -> nat." 285 | type: INDUCTIVE 286 | - text: 'Notation "x = y" := (eq x y) : type_scope.' 287 | type: NOTATION 288 | - text: 'Fixpoint add n m := match n with | 0 => m | S p => S (p + m) end where "n + m" := (add n m) : nat_scope.' 289 | type: NOTATION 290 | 291 | # 4th proof 292 | - text: "Theorem mult_0_plus : ∀ n m : nat, S n * m = 0 + (S n * m)." 293 | steps: 294 | - text: "\n Proof." 295 | goals: 296 | goals: 297 | goals: 298 | - hyps: [] 299 | ty: "∀ n m : nat, | n | * m = 0 + | n | * m" 300 | position: 301 | line: 46 302 | character: 4 303 | - text: "\n intros n m." 304 | goals: 305 | goals: 306 | goals: 307 | - hyps: [] 308 | ty: "∀ n m : nat, | n | * m = 0 + | n | * m" 309 | position: 310 | line: 47 311 | character: 6 312 | - text: "\n rewrite <- (Fst.plus_O_n (|n| * m))." 313 | goals: 314 | goals: 315 | goals: 316 | - hyps: 317 | - names: 318 | - n 319 | - m 320 | ty: nat 321 | ty: "| n | * m = 0 + | n | * m" 322 | position: 323 | line: 48 324 | character: 6 325 | context: 326 | - text: "Theorem plus_O_n : forall n:nat, n = 0 + n." 327 | type: THEOREM 328 | module: ["Extra", "Fst"] 329 | - text: 'Fixpoint mul n m := match n with | 0 => 0 | S p => m + p * m end where "n * m" := (mul n m) : nat_scope.' 330 | type: NOTATION 331 | - text: 'Notation "| a |" := (S a) (at level 30, right associativity).' 332 | type: NOTATION 333 | module: ["Extra", "Snd"] 334 | - text: "\n Compute {| Fst.fst := n; Fst.snd := n |}." 335 | goals: 336 | goals: 337 | goals: 338 | - hyps: 339 | - names: 340 | - n 341 | - m 342 | ty: nat 343 | ty: "| n | * m = | n | * m" 344 | position: 345 | line: 49 346 | character: 6 347 | context: 348 | - text: "Record example := mk_example { fst : nat; snd : nat }." 349 | type: RECORD 350 | module: ["Extra", "Fst"] 351 | - text: "\n reflexivity." 352 | goals: 353 | goals: 354 | goals: 355 | - hyps: 356 | - names: 357 | - n 358 | - m 359 | ty: nat 360 | ty: "| n | * m = | n | * m" 361 | position: 362 | line: 50 363 | character: 6 364 | - text: "\n Admitted." 365 | goals: 366 | position: 367 | line: 51 368 | character: 4 369 | context: 370 | - "8.19.x": 371 | text: "Notation \"∀ x .. y , P\" := (forall x, .. (forall y, P) ..) (at level 10, x binder, y binder, P at level 200, format \"'[ ' '[ ' ∀ x .. y ']' , '/' P ']'\") : type_scope." 372 | type: NOTATION 373 | "8.20.x": 374 | text: "Notation \"∀ x .. y , P\" := (forall x, .. (forall y, P) ..) (at level 10, x binder, y binder, P at level 200, format \"'[ ' '[ ' ∀ x .. y ']' , '/' P ']'\") : type_scope." 375 | type: NOTATION 376 | default: 377 | text: "Notation \"∀ x .. y , P\" := (forall x, .. (forall y, P) ..) (at level 200, x binder, y binder, right associativity, format \"'[ ' '[ ' ∀ x .. y ']' , '/' P ']'\") : type_scope." 378 | type: NOTATION 379 | - text: 'Notation "x = y" := (eq x y) : type_scope.' 380 | type: NOTATION 381 | - text: 'Fixpoint mul n m := match n with | 0 => 0 | S p => m + p * m end where "n * m" := (mul n m) : nat_scope.' 382 | type: NOTATION 383 | - text: "Inductive nat : Set := | O : nat | S : nat -> nat." 384 | type: INDUCTIVE 385 | - text: 'Fixpoint add n m := match n with | 0 => m | S p => S (p + m) end where "n + m" := (add n m) : nat_scope.' 386 | type: NOTATION 387 | 388 | -------------------------------------------------------------------------------- /coqpyt/tests/proof_file/test_cache.py: -------------------------------------------------------------------------------- 1 | from utility import * 2 | 3 | from coqpyt.coq.structs import Term 4 | from coqpyt.coq.proof_file import ProofFile 5 | 6 | 7 | class TestCache: 8 | @staticmethod 9 | def term_files_eq(terms1: dict[str, Term], terms2: dict[str, Term]) -> bool: 10 | if len(terms1) != len(terms2): 11 | return False 12 | for key in terms1: 13 | if key not in terms2: 14 | return False 15 | term1 = terms1[key] 16 | term2 = terms2[key] 17 | if term1 != term2: 18 | return False 19 | if term1.file_path != term2.file_path: 20 | return False 21 | return True 22 | 23 | def test_cache(self): 24 | with ProofFile( 25 | "tests/resources/test_imports/test_import.v", 26 | workspace="tests/resources/test_import", 27 | use_disk_cache=False, 28 | ) as pf: 29 | pf.run() 30 | no_cache_terms = pf.context.terms.copy() 31 | no_cache_libs = pf.context.libraries.copy() 32 | 33 | with ProofFile( 34 | "tests/resources/test_imports/test_import.v", 35 | workspace="tests/resources/test_import", 36 | use_disk_cache=True, 37 | ) as pf: 38 | pf.run() 39 | cache1_terms = pf.context.terms.copy() 40 | cache1_libs = pf.context.libraries.copy() 41 | 42 | with ProofFile( 43 | "tests/resources/test_imports/test_import.v", 44 | workspace="tests/resources/test_import", 45 | use_disk_cache=True, 46 | ) as pf: 47 | pf.run() 48 | cache2_terms = pf.context.terms.copy() 49 | cache2_libs = pf.context.libraries.copy() 50 | 51 | with ProofFile( 52 | "tests/resources/test_imports_copy/test_import.v", 53 | workspace="tests/resources/test_import_copy/", 54 | use_disk_cache=True, 55 | ) as pf: 56 | pf.run() 57 | cache3_terms = pf.context.terms.copy() 58 | cache3_libs = pf.context.libraries.copy() 59 | 60 | with ProofFile( 61 | "tests/resources/test_imports_copy/test_import.v", 62 | workspace="tests/resources/test_import_copy/", 63 | use_disk_cache=True, 64 | ) as pf: 65 | pf.run() 66 | cache4_terms = pf.context.terms.copy() 67 | cache4_libs = pf.context.libraries.copy() 68 | 69 | assert self.term_files_eq(no_cache_terms, cache1_terms) 70 | assert self.term_files_eq(cache1_terms, cache2_terms) 71 | assert not self.term_files_eq(cache2_terms, cache3_terms) 72 | assert self.term_files_eq(cache3_terms, cache4_terms) 73 | 74 | 75 | if __name__ == "__main__": 76 | test = TestCache() 77 | test.test_cache() 78 | -------------------------------------------------------------------------------- /coqpyt/tests/proof_file/test_proof_file.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from coqpyt.coq.lsp.structs import * 4 | from coqpyt.coq.exceptions import * 5 | from coqpyt.coq.structs import TermType 6 | 7 | from utility import * 8 | 9 | 10 | class TestProofValidFile(SetupProofFile): 11 | def setup_method(self, method): 12 | self.setup("test_valid.v") 13 | 14 | def test_valid_file(self): 15 | proofs = self.proof_file.proofs 16 | check_proofs( 17 | "tests/proof_file/expected/valid_file.yml", 18 | proofs, 19 | coq_version=self.coq_version, 20 | ) 21 | 22 | def test_exec(self): 23 | # Rollback whole file 24 | self.proof_file.exec(-self.proof_file.steps_taken) 25 | assert "plus_O_n" in self.proof_file.context.terms 26 | assert self.proof_file.context.get_term("plus_O_n").module == [] 27 | assert self.proof_file.context.curr_modules == [] 28 | 29 | # Check if roll back works for imports 30 | assert "∀ x .. y , P : type_scope" not in self.proof_file.context.terms 31 | self.proof_file.exec(1) 32 | assert "∀ x .. y , P : type_scope" in self.proof_file.context.terms 33 | 34 | steps = self.proof_file.exec(6) 35 | assert len(steps) == 6 36 | assert steps[-1].text == "\n intros n." 37 | assert self.proof_file.context.curr_modules == ["Out", "In"] 38 | assert "plus_O_n" in self.proof_file.context.terms 39 | assert self.proof_file.context.get_term("plus_O_n").module == ["Out", "In"] 40 | 41 | 42 | class TestProofImports(SetupProofFile): 43 | def setup_method(self, method): 44 | self.setup("test_imports/test_import.v", workspace="test_imports/") 45 | 46 | def test_imports(self): 47 | check_proofs( 48 | "tests/proof_file/expected/imports.yml", 49 | self.proof_file.proofs, 50 | coq_version=self.coq_version, 51 | ) 52 | 53 | def test_exec(self): 54 | # Rollback whole file 55 | self.proof_file.exec(-self.proof_file.steps_taken) 56 | # mult_0_plus is not defined because the import of test_import2 is not executed 57 | assert "mult_0_plus" not in self.proof_file.context.terms 58 | 59 | self.proof_file.exec(2) 60 | assert "mult_0_plus" in self.proof_file.context.terms 61 | # definition of test_import2 62 | assert ( 63 | self.proof_file.context.get_term("mult_0_plus").text 64 | == "Definition mult_0_plus : forall n m : nat, 0 + 0 + (S n * m) = S n * m." 65 | ) 66 | 67 | self.proof_file.exec(9) 68 | assert "mult_0_plus" in self.proof_file.context.terms 69 | # definition of test_import 70 | assert ( 71 | self.proof_file.context.get_term("mult_0_plus").text 72 | == "Definition mult_0_plus : ∀ n m : nat, 0 + (S n * m) = S n * m." 73 | ) 74 | 75 | 76 | class TestProofNonEndingProof(SetupProofFile): 77 | def setup_method(self, method): 78 | self.setup("test_non_ending_proof.v") 79 | 80 | def test_non_ending_proof(self): 81 | assert len(self.proof_file.open_proofs) == 1 82 | assert len(self.proof_file.open_proofs[0].steps) == 3 83 | 84 | 85 | class TestProofExistsNotation(SetupProofFile): 86 | def setup_method(self, method): 87 | self.setup("test_exists_notation.v") 88 | 89 | def test_exists_notation(self): 90 | """Checks if the exists notation is handled. The exists notation is defined 91 | with 'exists', but the search can be done without the '. 92 | """ 93 | assert ( 94 | self.proof_file.context.get_notation("exists _ .. _ , _", "type_scope").text 95 | == "Notation \"'exists' x .. y , p\" := (ex (fun x => .. (ex (fun y => p)) ..)) (at level 200, x binder, right associativity, format \"'[' 'exists' '/ ' x .. y , '/ ' p ']'\") : type_scope." 96 | ) 97 | 98 | 99 | class TestProofListNotation(SetupProofFile): 100 | def setup_method(self, method): 101 | self.setup("test_list_notation.v") 102 | 103 | # FIXME: Refer to issue #24: https://github.com/sr-lab/coqpyt/issues/24 104 | @pytest.mark.skip(reason="Skipping due to non-deterministic behaviour") 105 | def test_list_notation(self): 106 | check_proofs( 107 | "tests/proof_file/expected/list_notation.yml", self.proof_file.proofs 108 | ) 109 | 110 | 111 | class TestProofUnknownNotation(SetupProofFile): 112 | def setup_method(self, method): 113 | self.setup("test_unknown_notation.v") 114 | 115 | def test_unknown_notation(self): 116 | """Checks if it is able to handle the notation { _ } that is unknown for the 117 | Locate command because it is a default notation. 118 | """ 119 | with pytest.raises(NotationNotFoundException): 120 | assert self.proof_file.context.get_notation("{ _ }", "") 121 | 122 | 123 | class TestProofNthLocate(SetupProofFile): 124 | def setup_method(self, method): 125 | self.setup("test_nth_locate.v") 126 | 127 | def test_nth_locate(self): 128 | """Checks if it is able to handle notations that are not the first result 129 | returned by the Locate command. 130 | """ 131 | proof_file = self.proof_file 132 | assert len(proof_file.proofs) == 1 133 | proof = proof_file.proofs[0] 134 | 135 | theorem = "Lemma test : <> = <>." 136 | assert proof.text == theorem 137 | 138 | statement_context = [ 139 | ('Notation "x = y" := (eq x y) : type_scope.', TermType.NOTATION, []), 140 | ('Notation "<>" := BAnon : binder_scope.', TermType.NOTATION, []), 141 | ] 142 | compare_context(statement_context, proof.context) 143 | 144 | 145 | class TestProofNestedProofs(SetupProofFile): 146 | def setup_method(self, method): 147 | self.setup("test_nested_proofs.v") 148 | 149 | def test_nested_proofs(self): 150 | proof_file = self.proof_file 151 | proofs = proof_file.proofs 152 | assert len(proofs) == 2 153 | 154 | steps = ["\n intros n.", "\n simpl; reflexivity.", "\n Qed."] 155 | assert len(proofs[0].steps) == len(steps) 156 | for i, step in enumerate(proofs[0].steps): 157 | assert step.text == steps[i] 158 | 159 | theorem = "Theorem mult_0_plus : forall n m : nat, S n * m = 0 + (S n * m)." 160 | steps = [ 161 | "\nProof.", 162 | "\nintros n m.", 163 | "\n\nrewrite <- (plus_O_n ((S n) * m)).", 164 | "\nreflexivity.", 165 | "\nQed.", 166 | ] 167 | assert proofs[1].text == theorem 168 | assert len(proofs[1].steps) == len(steps) 169 | for i, step in enumerate(proofs[1].steps): 170 | assert step.text == steps[i] 171 | 172 | proofs = proof_file.open_proofs 173 | assert len(proofs) == 2 174 | 175 | steps = [ 176 | "\n intros n.", 177 | "\n simpl; reflexivity.", 178 | ] 179 | assert len(proofs[0].steps) == 2 180 | for i, step in enumerate(proofs[0].steps): 181 | assert step.text == steps[i] 182 | 183 | steps = [ 184 | "\n intros n.", 185 | "\n simpl; reflexivity.", 186 | ] 187 | assert len(proofs[1].steps) == 2 188 | for i, step in enumerate(proofs[1].steps): 189 | assert step.text == steps[i] 190 | 191 | 192 | class TestProofTheoremTokens(SetupProofFile): 193 | def setup_method(self, method): 194 | self.setup("test_theorem_tokens.v") 195 | 196 | def test_theorem_tokens(self): 197 | proofs = self.proof_file.proofs 198 | assert len(proofs) == 7 199 | assert list(map(lambda proof: proof.type, proofs)) == [ 200 | TermType.REMARK, 201 | TermType.FACT, 202 | TermType.COROLLARY, 203 | TermType.PROPOSITION, 204 | TermType.PROPERTY, 205 | TermType.THEOREM, 206 | TermType.LEMMA, 207 | ] 208 | 209 | 210 | class TestProofBullets(SetupProofFile): 211 | def setup_method(self, method): 212 | self.setup("test_bullets.v") 213 | 214 | def test_bullets(self): 215 | proofs = self.proof_file.proofs 216 | assert len(proofs) == 1 217 | steps = [ 218 | "\nProof.", 219 | "\n intros x y.", 220 | " split.", 221 | "\n -", 222 | "reflexivity.", 223 | "\n -", 224 | " reflexivity.", 225 | "\nQed.", 226 | ] 227 | assert len(proofs[0].steps) == len(steps) 228 | for i, step in enumerate(proofs[0].steps): 229 | assert step.text == steps[i] 230 | 231 | 232 | class TestProofObligation(SetupProofFile): 233 | def setup_method(self, method): 234 | self.setup("test_obligation.v") 235 | 236 | def test_obligation(self): 237 | # Rollback whole file (except slow import) 238 | self.proof_file.exec(-self.proof_file.steps_taken + 1) 239 | proofs = self.proof_file.proofs 240 | assert len(proofs) == 0 241 | self.proof_file.run() 242 | proofs = self.proof_file.proofs 243 | assert len(proofs) == 13 244 | 245 | statement_context = [ 246 | ( 247 | "Inductive nat : Set := | O : nat | S : nat -> nat.", 248 | TermType.INDUCTIVE, 249 | [], 250 | ), 251 | ("Notation dec := sumbool_of_bool.", TermType.NOTATION, []), 252 | ( 253 | "Fixpoint leb n m : bool := match n, m with | 0, _ => true | _, 0 => false | S n', S m' => leb n' m' end.", 254 | TermType.FIXPOINT, 255 | [], 256 | ), 257 | ("Notation pred := Nat.pred (only parsing).", TermType.NOTATION, []), 258 | ( 259 | 'Notation "{ x : A | P }" := (sig (A:=A) (fun x => P)) : type_scope.', 260 | TermType.NOTATION, 261 | [], 262 | ), 263 | ('Notation "x = y" := (eq x y) : type_scope.', TermType.NOTATION, []), 264 | ] 265 | texts = [ 266 | "Obligation 2 of id2.", 267 | "Next Obligation of id2.", 268 | "Obligation 2 of id3 : type with reflexivity.", 269 | "Next Obligation of id3 with reflexivity.", 270 | "Next Obligation.", 271 | "Next Obligation with reflexivity.", 272 | "Obligation 1.", 273 | "Obligation 2 : type with reflexivity.", 274 | "Obligation 1 of id with reflexivity.", 275 | "Obligation 1 of id : type.", 276 | "Obligation 2 : type.", 277 | ] 278 | programs = [ 279 | ("#[global, program]", "id2", "S (pred n)"), 280 | ("#[global, program]", "id2", "S (pred n)"), 281 | ("Local Program", "id3", "S (pred n)"), 282 | ("Local Program", "id3", "S (pred n)"), 283 | ("#[local, program]", "id1", "S (pred n)"), 284 | ("#[local, program]", "id1", "S (pred n)"), 285 | ("Global Program", "id4", "S (pred n)"), 286 | ("Global Program", "id4", "S (pred n)"), 287 | ("#[program]", "id", "pred (S n)"), 288 | ("Program", "id", "S (pred n)"), 289 | ("Program", "id", "S (pred n)"), 290 | ] 291 | 292 | obligations = proofs[:8] + proofs[-3:] 293 | for i, proof in enumerate(obligations): 294 | compare_context(statement_context, proof.context) 295 | assert proof.text == texts[i] 296 | assert proof.program is not None 297 | assert ( 298 | proof.program.text 299 | == programs[i][0] 300 | + " Definition " 301 | + programs[i][1] 302 | + " (n : nat) : { x : nat | x = n } := if dec (Nat.leb n 0) then 0%nat else " 303 | + programs[i][2] 304 | + "." 305 | ) 306 | assert len(proof.steps) == 2 307 | assert proof.steps[0].text == "\n dummy_tactic n e." 308 | 309 | statement_context = [ 310 | ( 311 | "Inductive nat : Set := | O : nat | S : nat -> nat.", 312 | TermType.INDUCTIVE, 313 | [], 314 | ), 315 | ('Notation "x = y" := (eq x y) : type_scope.', TermType.NOTATION, []), 316 | ( 317 | "Program Definition id (n : nat) : { x : nat | x = n } := if dec (Nat.leb n 0) then 0%nat else S (pred n).", 318 | TermType.DEFINITION, 319 | ["Out"], 320 | ), 321 | ] 322 | texts = [ 323 | "Program Lemma id_lemma (n : nat) : id n = n.", 324 | "Program Theorem id_theorem (n : nat) : id n = n.", 325 | ] 326 | for i, proof in enumerate(proofs[8:-3]): 327 | compare_context(statement_context, proof.context) 328 | assert proof.text == texts[i] 329 | assert proof.program is None 330 | assert len(proof.steps) == 3 331 | assert proof.steps[1].text == " destruct n; try reflexivity." 332 | 333 | 334 | class TestProofModuleType(SetupProofFile): 335 | def setup_method(self, method): 336 | self.setup("test_module_type.v") 337 | 338 | def test_proof_module_type(self): 339 | # We ignore proofs inside a Module Type since they can't be used outside 340 | # and should be overriden. 341 | assert len(self.proof_file.proofs) == 1 342 | 343 | 344 | class TestProofTypeClass(SetupProofFile): 345 | def setup_method(self, method): 346 | self.setup("test_type_class.v") 347 | 348 | def test_type_class(self): 349 | proof_file = self.proof_file 350 | assert len(proof_file.proofs) == 2 351 | assert len(proof_file.proofs[0].steps) == 4 352 | assert ( 353 | proof_file.proofs[0].text 354 | == "#[refine] Global Instance unit_EqDec : TypeClass.EqDecNew unit := { eqb_new x y := true }." 355 | ) 356 | 357 | context = [ 358 | ( 359 | "Class EqDecNew (A : Type) := { eqb_new : A -> A -> bool ; eqb_leibniz_new : forall x y, eqb_new x y = true -> x = y ; eqb_ident_new : forall x, eqb_new x x = true }.", 360 | TermType.CLASS, 361 | ["TypeClass"], 362 | ), 363 | ("Inductive unit : Set := tt : unit.", TermType.INDUCTIVE, []), 364 | ( 365 | "Inductive bool : Set := | true : bool | false : bool.", 366 | TermType.INDUCTIVE, 367 | [], 368 | ), 369 | ] 370 | compare_context(context, proof_file.proofs[0].context) 371 | 372 | assert ( 373 | proof_file.proofs[1].text 374 | == "Instance test : TypeClass.EqDecNew unit -> TypeClass.EqDecNew unit." 375 | ) 376 | 377 | context = [ 378 | ( 379 | 'Notation "A -> B" := (forall (_ : A), B) : type_scope.', 380 | TermType.NOTATION, 381 | [], 382 | ), 383 | ( 384 | "Class EqDecNew (A : Type) := { eqb_new : A -> A -> bool ; eqb_leibniz_new : forall x y, eqb_new x y = true -> x = y ; eqb_ident_new : forall x, eqb_new x x = true }.", 385 | TermType.CLASS, 386 | ["TypeClass"], 387 | ), 388 | ("Inductive unit : Set := tt : unit.", TermType.INDUCTIVE, []), 389 | ] 390 | compare_context(context, proof_file.proofs[1].context) 391 | 392 | 393 | class TestProofGoal(SetupProofFile): 394 | def setup_method(self, method): 395 | self.setup("test_goal.v") 396 | 397 | def test_goal(self): 398 | assert len(self.proof_file.proofs) == 3 399 | goals = [ 400 | "Definition ignored : forall P Q: Prop, (P -> Q) -> P -> Q.", 401 | "Goal forall P Q: Prop, (P -> Q) -> P -> Q.", 402 | "Goal forall P Q: Prop, (P -> Q) -> P -> Q.", 403 | ] 404 | for i, proof in enumerate(self.proof_file.proofs): 405 | assert proof.text == goals[i] 406 | compare_context( 407 | [ 408 | ( 409 | 'Notation "A -> B" := (forall (_ : A), B) : type_scope.', 410 | TermType.NOTATION, 411 | [], 412 | ) 413 | ], 414 | proof.context, 415 | ) 416 | 417 | 418 | class TestProofCmd(SetupProofFile): 419 | def setup_method(self, method): 420 | self.setup("test_proof_cmd.v") 421 | 422 | def test_proof_cmd(self): 423 | assert len(self.proof_file.proofs) == 3 424 | goals = [ 425 | "Goal ∃ (m : nat), S m = n.", 426 | "Goal ∃ (m : nat), S m = n.", 427 | "Goal nat.", 428 | ] 429 | proofs = [ 430 | "\n Proof using Hn.", 431 | "\n Proof with auto.", 432 | "\n Proof 0.", 433 | ] 434 | for i, proof in enumerate(self.proof_file.proofs): 435 | assert proof.text == goals[i] 436 | assert proof.steps[0].text == proofs[i] 437 | 438 | 439 | class TestProofSection(SetupProofFile): 440 | def setup_method(self, method): 441 | self.setup("test_section_terms.v") 442 | 443 | def test_section(self): 444 | assert len(self.proof_file.proofs) == 1 445 | assert self.proof_file.proofs[0].text == "Let ignored : nat." 446 | assert len(self.proof_file.context.local_terms) == 0 447 | 448 | 449 | class TestModuleInline(SetupProofFile): 450 | def setup_method(self, method): 451 | self.setup("test_module_inline.v") 452 | 453 | def test_module_import(self): 454 | self.proof_file.exec(-self.proof_file.steps_taken) 455 | self.proof_file.run() 456 | assert self.proof_file.context.curr_modules == [] 457 | assert not self.proof_file.context.in_module_type 458 | -------------------------------------------------------------------------------- /coqpyt/tests/proof_file/utility.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import shutil 4 | import subprocess 5 | import tempfile 6 | import uuid 7 | import yaml 8 | 9 | from abc import ABC, abstractmethod 10 | from typing import Tuple, List, Dict, Union, Any 11 | 12 | from coqpyt.coq.proof_file import ProofFile, ProofStep, ProofTerm 13 | from coqpyt.coq.structs import TermType, Term 14 | from coqpyt.coq.lsp.structs import * 15 | 16 | 17 | class SetupProofFile(ABC): 18 | def setup(self, file_path, workspace=None, use_disk_cache: bool = False): 19 | if workspace is not None: 20 | self.workspace = os.path.join( 21 | tempfile.gettempdir(), "test" + str(uuid.uuid4()).replace("-", "") 22 | ) 23 | shutil.copytree(os.path.join("tests/resources", workspace), self.workspace) 24 | run = subprocess.run( 25 | f"cd {self.workspace} && make", shell=True, capture_output=True 26 | ) 27 | assert run.returncode == 0 28 | self.file_path = os.path.join(self.workspace, os.path.basename(file_path)) 29 | else: 30 | self.workspace = None 31 | new_path = os.path.join( 32 | tempfile.gettempdir(), 33 | "test" + str(uuid.uuid4()).replace("-", "") + ".v", 34 | ) 35 | shutil.copyfile(os.path.join("tests/resources", file_path), new_path) 36 | self.file_path = new_path 37 | 38 | uri = "file://" + self.file_path 39 | self.proof_file = ProofFile( 40 | self.file_path, 41 | timeout=60, 42 | workspace=self.workspace, 43 | use_disk_cache=use_disk_cache, 44 | ) 45 | self.proof_file.run() 46 | self.versionId = VersionedTextDocumentIdentifier(uri, 1) 47 | 48 | output = subprocess.check_output(f"coqtop -v", shell=True) 49 | self.coq_version = output.decode("utf-8").split("\n")[0].split()[-1] 50 | 51 | @abstractmethod 52 | def setup_method(self, method): 53 | pass 54 | 55 | def teardown_method(self, method): 56 | if self.workspace is not None: 57 | subprocess.run( 58 | f"cd {self.workspace} && make clean", shell=True, capture_output=True 59 | ) 60 | self.proof_file.close() 61 | os.remove(self.file_path) 62 | 63 | 64 | def compare_context( 65 | test_context: List[Tuple[str, TermType, List[str]]], context: List[Term] 66 | ): 67 | assert len(test_context) == len(context) 68 | for i in range(len(context)): 69 | assert test_context[i][0] == context[i].text 70 | assert test_context[i][1] == context[i].type 71 | assert test_context[i][2] == context[i].module 72 | 73 | 74 | def check_context(test_context: List[Dict[str, Union[str, List]]], context: List[Term]): 75 | assert len(test_context) == len(context) 76 | for i in range(len(context)): 77 | assert test_context[i]["text"] == context[i].text 78 | assert TermType[test_context[i]["type"]] == context[i].type 79 | if "module" not in test_context[i]: 80 | test_context[i]["module"] = [] 81 | assert test_context[i]["module"] == context[i].module 82 | 83 | 84 | def check_goal(test_goal: Dict, goal: Goal): 85 | assert test_goal["ty"] == goal.ty 86 | assert len(test_goal["hyps"]) == len(goal.hyps) 87 | for j in range(len(goal.hyps)): 88 | assert test_goal["hyps"][j]["ty"] == goal.hyps[j].ty 89 | assert len(test_goal["hyps"][j]["names"]) == len(goal.hyps[j].names) 90 | for k in range(len(goal.hyps[j].names)): 91 | assert test_goal["hyps"][j]["names"][k] == goal.hyps[j].names[k] 92 | 93 | 94 | def check_step(test_step: Dict[str, Any], step: ProofStep): 95 | assert test_step["text"] == step.text 96 | goals = test_step["goals"] 97 | 98 | assert goals["position"]["line"] == step.goals.position.line 99 | assert goals["position"]["character"] == step.goals.position.character 100 | assert len(goals["messages"]) == len(step.goals.messages) 101 | for i in range(len(step.goals.messages)): 102 | assert goals["messages"][i] == step.goals.messages[i].text 103 | 104 | assert len(goals["goals"]["goals"]) == len(step.goals.goals.goals) 105 | for i in range(len(step.goals.goals.goals)): 106 | check_goal(goals["goals"]["goals"][i], step.goals.goals.goals[i]) 107 | 108 | # Check stack 109 | assert len(goals["goals"]["stack"]) == len(step.goals.goals.stack) 110 | for i in range(len(step.goals.goals.stack)): 111 | assert len(goals["goals"]["stack"][i][0]) == len(step.goals.goals.stack[i][0]) 112 | for j in range(len(step.goals.goals.stack[i][0])): 113 | check_goal( 114 | goals["goals"]["stack"][i][0][j], step.goals.goals.stack[i][0][j] 115 | ) 116 | 117 | assert len(goals["goals"]["stack"][i][1]) == len(step.goals.goals.stack[i][1]) 118 | for j in range(len(step.goals.goals.stack[i][1])): 119 | check_goal( 120 | goals["goals"]["stack"][i][1][j], step.goals.goals.stack[i][1][j] 121 | ) 122 | 123 | # Check shelf 124 | assert len(goals["goals"]["shelf"]) == len(step.goals.goals.shelf) 125 | for i in range(len(step.goals.goals.shelf)): 126 | check_goal(goals["goals"]["shelf"][i], step.goals.goals.shelf[i]) 127 | 128 | # Check given_up 129 | assert len(goals["goals"]["given_up"]) == len(step.goals.goals.given_up) 130 | for i in range(len(step.goals.goals.given_up)): 131 | check_goal(goals["goals"]["given_up"][i], step.goals.goals.given_up[i]) 132 | 133 | check_context(test_step["context"], step.context) 134 | 135 | if "range" in test_step: 136 | test_range = test_step["range"] 137 | step_range = step.ast.range 138 | assert test_range["start"]["line"] == step_range.start.line 139 | assert test_range["start"]["character"] == step_range.start.character 140 | assert test_range["end"]["line"] == step_range.end.line 141 | assert test_range["end"]["character"] == step_range.end.character 142 | 143 | 144 | def check_proof(test_proof: Dict, proof: ProofTerm): 145 | check_context(test_proof["context"], proof.context) 146 | assert len(test_proof["steps"]) == len(proof.steps) 147 | if "program" in test_proof: 148 | assert proof.program is not None 149 | assert test_proof["program"] == proof.program.text 150 | for j, step in enumerate(test_proof["steps"]): 151 | check_step(step, proof.steps[j]) 152 | 153 | 154 | def check_proofs( 155 | yaml_file: str, proofs: List[ProofTerm], coq_version: Optional[str] = None 156 | ): 157 | test_proofs = get_test_proofs(yaml_file, coq_version) 158 | assert len(proofs) == len(test_proofs["proofs"]) 159 | for i, test_proof in enumerate(test_proofs["proofs"]): 160 | check_proof(test_proof, proofs[i]) 161 | 162 | 163 | def add_step_defaults(step): 164 | if "goals" not in step: 165 | step["goals"] = {} 166 | if "messages" not in step["goals"]: 167 | step["goals"]["messages"] = [] 168 | if "goals" not in step["goals"]: 169 | step["goals"]["goals"] = {} 170 | if "goals" not in step["goals"]["goals"]: 171 | step["goals"]["goals"]["goals"] = [] 172 | if "stack" not in step["goals"]["goals"]: 173 | step["goals"]["goals"]["stack"] = [] 174 | if "shelf" not in step["goals"]["goals"]: 175 | step["goals"]["goals"]["shelf"] = [] 176 | if "given_up" not in step["goals"]["goals"]: 177 | step["goals"]["goals"]["given_up"] = [] 178 | if "context" not in step: 179 | step["context"] = [] 180 | 181 | 182 | def get_context_by_version(context: List[Dict[str, Any]], coq_version: str): 183 | res = [] 184 | 185 | for term in context: 186 | for key in term: 187 | if re.match(key.replace("x", "[0-9]+"), coq_version): 188 | res.append(term[key]) 189 | break 190 | else: 191 | res.append(term["default"] if "default" in term else term) 192 | 193 | return res 194 | 195 | 196 | def get_test_proofs(yaml_file: str, coq_version: Optional[str] = None): 197 | with open(yaml_file, "r") as f: 198 | test_proofs = yaml.safe_load(f) 199 | for test_proof in test_proofs["proofs"]: 200 | if "context" not in test_proof: 201 | test_proof["context"] = [] 202 | if coq_version is not None: 203 | test_proof["context"] = get_context_by_version( 204 | test_proof["context"], coq_version 205 | ) 206 | for step in test_proof["steps"]: 207 | add_step_defaults(step) 208 | return test_proofs 209 | -------------------------------------------------------------------------------- /coqpyt/tests/resources/.gitignore: -------------------------------------------------------------------------------- 1 | test_valid_delete.v 2 | test_valid_add.v -------------------------------------------------------------------------------- /coqpyt/tests/resources/test test/test_error.v: -------------------------------------------------------------------------------- 1 | Import NonExistentModule. -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_add_open_proof.v: -------------------------------------------------------------------------------- 1 | Definition x := 0. 2 | 3 | Theorem plus_O_n : forall n:nat, 0 + n = n. 4 | Proof. 5 | intros n. 6 | reflexivity. 7 | Qed. -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_bullets.v: -------------------------------------------------------------------------------- 1 | Theorem bullets: forall x y: nat, x = x /\ y = y. 2 | Proof. 3 | intros x y. split. 4 | -reflexivity. 5 | - reflexivity. 6 | Qed. 7 | -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_change_empty.v: -------------------------------------------------------------------------------- 1 | Lemma exists_min: True = True. 2 | Proof. -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_change_goals.v: -------------------------------------------------------------------------------- 1 | Lemma add_0: forall n, 0 + n = n. 2 | Proof. 3 | induction n. 4 | Admitted. -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_change_obligation.v: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sr-lab/coqpyt/75a3cca35f6d0f4043b94c26948afde8869ebd77/coqpyt/tests/resources/test_change_obligation.v -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_change_with_notation.v: -------------------------------------------------------------------------------- 1 | Require Import Coq.Lists.List. 2 | Require Import Coq.Init.Nat. 3 | 4 | Fixpoint min (l : (list nat)) : option nat := 5 | match l with 6 | | nil => None 7 | | h :: tl => match (min tl) with 8 | | None => Some h 9 | | Some m => if (h nil) -> exists h, min(l) = Some(h). 15 | Proof. 16 | intros. 17 | induction l. 18 | - contradiction. 19 | - exists a. 20 | simpl. 21 | destruct (min l). 22 | + 23 | Admitted. -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_delete_qed.v: -------------------------------------------------------------------------------- 1 | Definition x := 0. 2 | 3 | Theorem delete_qed : forall n:nat, 0 + n = n. 4 | Proof. 5 | intros n. 6 | reflexivity. 7 | 8 | Theorem delete_qed2 : forall n:nat, 0 + n = n. 9 | Proof. 10 | intros n. 11 | reflexivity. 12 | Qed. 13 | 14 | Theorem delete_qed3 : forall n:nat, 0 + n = n. 15 | Proof. 16 | intros n. 17 | reflexivity. 18 | 19 | Theorem delete_qed4 : forall n:nat, 0 + n = n. 20 | Proof. 21 | intros n. 22 | reflexivity. 23 | Qed. -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_derive.v: -------------------------------------------------------------------------------- 1 | Require Import Coq.derive.Derive. 2 | 3 | Derive incr 4 | SuchThat (forall n, incr n = plus 1 n) 5 | As incr_correct. 6 | Proof. intros n. simpl. subst incr. reflexivity. Qed. 7 | 8 | Inductive Le : nat -> nat -> Set := 9 | | LeO : forall n:nat, Le 0 n 10 | | LeS : forall n m:nat, Le n m -> Le (S n) (S m). 11 | Derive Inversion leminv1 with (forall n m:nat, Le (S n) m) Sort Prop. 12 | Derive Inversion_clear leminv2 with (forall n m:nat, Le (S n) m) Sort Prop. 13 | Derive Dependent Inversion leminv3 with (forall n m:nat, Le (S n) m) Sort Prop. 14 | Derive Dependent Inversion_clear leminv4 with (forall n m:nat, Le (S n) m) Sort Prop. -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_equations.v: -------------------------------------------------------------------------------- 1 | From Equations Require Import Equations. 2 | 3 | Equations? f (n : nat) : nat := 4 | f 0 := 42 ; 5 | f (S m) with f m := { f (S m) IH := _ }. 6 | Proof. intros. exact IH. Defined. -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_exists_notation.v: -------------------------------------------------------------------------------- 1 | Set Implicit Arguments. 2 | 3 | Section SEQUENCES. 4 | 5 | Variable A: Type. (**r the type of states *) 6 | Variable R: A -> A -> Prop. (**r the transition relation between states *) 7 | 8 | Inductive star: A -> A -> Prop := 9 | | star_refl: forall a, 10 | star a a 11 | | star_step: forall a b c, 12 | R a b -> star b c -> star a c. 13 | 14 | Definition all_seq_inf (a: A) : Prop := 15 | forall b, star a b -> exists c, R b c. 16 | -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_get_notation.v: -------------------------------------------------------------------------------- 1 | Notation "'|' AB '|' BC '|'" := (minus AB BC) : test_scope. 2 | Notation "'_' AB '_' BC '_'" := (plus AB BC) : test_scope. 3 | Notation "'C_D' A_B 'C_D'" := (plus A_B A_B) : test_scope. 4 | Infix "++" := app (right associativity, at level 60) : list_scope. 5 | -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_goal.v: -------------------------------------------------------------------------------- 1 | (* http://d.hatena.ne.jp/hzkr/20100902 *) 2 | 3 | Definition ignored : forall P Q: Prop, (P -> Q) -> P -> Q. 4 | Proof. 5 | intros. (* 全部剥がす *) 6 | apply H. 7 | exact H0. (* apply H0 でも同じ *) 8 | Save opaque. 9 | 10 | 11 | Goal forall P Q: Prop, (P -> Q) -> P -> Q. 12 | Proof. 13 | intros p q f. (* 名前の数だけ剥がす *) 14 | assumption. (* exact f でも同じ *) 15 | Qed. 16 | 17 | 18 | Goal forall P Q: Prop, (P -> Q) -> P -> Q. 19 | Proof. 20 | exact (fun p q f x => f x). 21 | Defined transparent. -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_imports/.gitignore: -------------------------------------------------------------------------------- 1 | *.vok 2 | *.vos 3 | *.vo 4 | *.glob 5 | .*.aux 6 | Makefile.* 7 | .Makefile.* 8 | -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_imports/Makefile: -------------------------------------------------------------------------------- 1 | # Default target 2 | all: Makefile.coq 3 | +@$(MAKE) -f Makefile.coq all 4 | .PHONY: all 5 | 6 | # Permit local customization 7 | -include Makefile.local 8 | 9 | # Forward most targets to Coq makefile (with some trick to make this phony) 10 | %: Makefile.coq phony 11 | @#echo "Forwarding $@" 12 | +@$(MAKE) -f Makefile.coq $@ 13 | phony: ; 14 | .PHONY: phony 15 | 16 | clean: Makefile.coq 17 | +@$(MAKE) -f Makefile.coq clean 18 | @# Make sure not to enter the `_opam` folder. 19 | rm .*.aux Makefile.* 20 | .PHONY: clean 21 | 22 | # Create Coq Makefile. 23 | Makefile.coq: _CoqProject Makefile 24 | "$(COQBIN)coq_makefile" -f _CoqProject -o Makefile.coq $(EXTRA_COQFILES) 25 | 26 | # Some files that do *not* need to be forwarded to Makefile.coq. 27 | # ("::" lets Makefile.local overwrite this.) 28 | Makefile Makefile.local _CoqProject $(OPAMFILES):: ; -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_imports/_CoqProject: -------------------------------------------------------------------------------- 1 | -R . TestImport 2 | test_import2.v 3 | test_import.v 4 | -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_imports/test_import.v: -------------------------------------------------------------------------------- 1 | Require Import Coq.Unicode.Utf8. 2 | Require Import test_import2. 3 | 4 | Ltac reduce_eq := simpl; reflexivity. 5 | 6 | Local Theorem plus_O_n : forall n:nat, 0 + n = n. 7 | Proof. 8 | intros n. 9 | Print plus. 10 | Print Nat.add. 11 | reduce_eq. 12 | Qed. 13 | 14 | Definition mult_0_plus : ∀ n m : nat, 15 | 0 + (S n * m) = S n * m. 16 | Proof. 17 | intros n m. 18 | rewrite -> (plus_O_n (S n * m)). 19 | Locate test_import2.plus_O_n. 20 | Locate Peano.plus_O_n. 21 | reflexivity. 22 | Defined. -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_imports/test_import2.v: -------------------------------------------------------------------------------- 1 | Ltac reduce_eq := simpl; reflexivity. 2 | 3 | Local Theorem plus_O_n : forall n:nat, 0 + 0 + n = n. 4 | Proof. 5 | intros n. 6 | Print plus. 7 | Print Nat.add. 8 | reduce_eq. 9 | Qed. 10 | 11 | Definition mult_0_plus : forall n m : nat, 12 | 0 + 0 + (S n * m) = S n * m. 13 | Proof. 14 | intros n m. 15 | rewrite -> (plus_O_n (S n * m)). 16 | reflexivity. 17 | Defined. -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_imports_copy/.gitignore: -------------------------------------------------------------------------------- 1 | *.vok 2 | *.vos 3 | *.vo 4 | *.glob 5 | .*.aux 6 | Makefile.* 7 | .Makefile.* 8 | -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_imports_copy/Makefile: -------------------------------------------------------------------------------- 1 | # Default target 2 | all: Makefile.coq 3 | +@$(MAKE) -f Makefile.coq all 4 | .PHONY: all 5 | 6 | # Permit local customization 7 | -include Makefile.local 8 | 9 | # Forward most targets to Coq makefile (with some trick to make this phony) 10 | %: Makefile.coq phony 11 | @#echo "Forwarding $@" 12 | +@$(MAKE) -f Makefile.coq $@ 13 | phony: ; 14 | .PHONY: phony 15 | 16 | clean: Makefile.coq 17 | +@$(MAKE) -f Makefile.coq clean 18 | @# Make sure not to enter the `_opam` folder. 19 | rm .*.aux Makefile.* 20 | .PHONY: clean 21 | 22 | # Create Coq Makefile. 23 | Makefile.coq: _CoqProject Makefile 24 | "$(COQBIN)coq_makefile" -f _CoqProject -o Makefile.coq $(EXTRA_COQFILES) 25 | 26 | # Some files that do *not* need to be forwarded to Makefile.coq. 27 | # ("::" lets Makefile.local overwrite this.) 28 | Makefile Makefile.local _CoqProject $(OPAMFILES):: ; -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_imports_copy/_CoqProject: -------------------------------------------------------------------------------- 1 | -R . TestImport 2 | test_import2.v 3 | test_import.v 4 | -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_imports_copy/test_import.v: -------------------------------------------------------------------------------- 1 | Require Import Coq.Unicode.Utf8. 2 | Require Import test_import2. 3 | 4 | Ltac reduce_eq := simpl; reflexivity. 5 | 6 | Local Theorem plus_O_n : forall n:nat, 0 + n = n. 7 | Proof. 8 | intros n. 9 | Print plus. 10 | Print Nat.add. 11 | reduce_eq. 12 | Qed. 13 | 14 | Definition mult_0_plus : ∀ n m : nat, 15 | 0 + (S n * m) = S n * m. 16 | Proof. 17 | intros n m. 18 | rewrite -> (plus_O_n (S n * m)). 19 | Locate test_import2.plus_O_n. 20 | Locate Peano.plus_O_n. 21 | reflexivity. 22 | Defined. -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_imports_copy/test_import2.v: -------------------------------------------------------------------------------- 1 | Ltac reduce_eq := simpl; reflexivity. 2 | 3 | Local Theorem plus_O_n : forall n:nat, 0 + 0 + n = n. 4 | Proof. 5 | intros n. 6 | Print plus. 7 | Print Nat.add. 8 | reduce_eq. 9 | Qed. 10 | 11 | Definition mult_0_plus : forall n m : nat, 12 | 0 + 0 + (S n * m) = S n * m. 13 | Proof. 14 | intros n m. 15 | rewrite -> (plus_O_n (S n * m)). 16 | reflexivity. 17 | Defined. -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_invalid_1.v: -------------------------------------------------------------------------------- 1 | Require Import Coq.Unicode.Utf8. 2 | 3 | Ltac reduce_eq := simpl; reflexivity. 4 | 5 | Theorem plus_O_n : forall n:nat, 0 + n = n. 6 | Proof. 7 | intros n. 8 | Print plus. 9 | Print Nat.add. 10 | reduce_eq. 11 | Qed. 12 | 13 | Theorem mult_0_plus : ∀ n m : nat, 14 | 0 + (S n * m) = S n * m. 15 | Proof. 16 | (* intros n m. *) 17 | rewrite -> plus_O_n. 18 | Compute True /\ True. 19 | reflexivity. 20 | Qed. 21 | -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_invalid_2.v: -------------------------------------------------------------------------------- 1 | Require Import Coq.Unicode.Utf8. 2 | 3 | Ltac reduce_eq := simpl; reflexivity. 4 | 5 | Theorem plus_O_n : forall n:nat, 0 + n = n. 6 | Proof. 7 | intros n. 8 | Print plus. 9 | Print Nat.add. 10 | reduce_eq. 11 | Qed. 12 | 13 | Theorem mult_0_plus : ∀ n m : nat, 14 | 0 + (S n * m) = S n * m. 15 | Proof. 16 | intros n m. 17 | rewrite -> plus_O_n. 18 | Compute True /\ True. 19 | reflexivity. 20 | Qed 21 | -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_invalid_changes.v: -------------------------------------------------------------------------------- 1 | Require Import Coq.Unicode.Utf8. 2 | 3 | Module A. 4 | Definition x := 2. 5 | End A. 6 | 7 | Goal ∀ (A : nat), A = A. 8 | Proof. 9 | Print A. 10 | reflexivity. 11 | Qed. -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_list_notation.v: -------------------------------------------------------------------------------- 1 | Require Import List. 2 | Import List.ListNotations. 3 | 4 | Goal [1] ++ [2] = [1; 2]. 5 | Proof. reflexivity. Qed. -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_module_inline.v: -------------------------------------------------------------------------------- 1 | Module Test2. 2 | End Test2. 3 | 4 | Module Test. 5 | Module Import ValidSafe := Test2. 6 | End Test. 7 | 8 | Module Type Test3. 9 | End Test3. 10 | 11 | Module Type Test4 := Test3. -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_module_type.v: -------------------------------------------------------------------------------- 1 | Module Type TestModuleType. 2 | Module TestModuleType. 3 | End TestModuleType. 4 | 5 | Section TestModuleType. 6 | End TestModuleType. 7 | 8 | Parameter NAT_MIN : nat. 9 | Definition x := 2. 10 | 11 | Theorem plus_O_n_new : forall n:nat, 0 + n = n. 12 | Proof. 13 | intros n. 14 | simpl; reflexivity. 15 | Qed. 16 | End TestModuleType. 17 | 18 | Theorem plus_O_n : forall n:nat, 0 + n = n. 19 | Proof. 20 | intros n. 21 | simpl; reflexivity. 22 | Qed. -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_nested_proofs.v: -------------------------------------------------------------------------------- 1 | Set Nested Proofs Allowed. 2 | 3 | Theorem mult_0_plus : forall n m : nat, 4 | S n * m = 0 + (S n * m). 5 | Proof. 6 | intros n m. 7 | 8 | Ltac reduce_eq := simpl; reflexivity. 9 | 10 | Theorem plus_O_n : forall n:nat, n = 0 + n. 11 | intros n. 12 | simpl; reflexivity. 13 | Qed. 14 | 15 | rewrite <- (plus_O_n ((S n) * m)). 16 | reflexivity. 17 | Qed. 18 | 19 | Theorem plus_O_n_2 : forall n:nat, n = 0 + n. 20 | intros n. 21 | simpl; reflexivity. 22 | Theorem plus_O_n_3 : forall n:nat, n = 0 + n. 23 | intros n. 24 | simpl; reflexivity. -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_non_ending_proof.v: -------------------------------------------------------------------------------- 1 | Theorem x : forall n:nat, n=n. 2 | intro n. 3 | destruct (plus n 1) eqn:Y. 4 | reflexivity. -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_nth_locate.v: -------------------------------------------------------------------------------- 1 | Inductive binder := BAnon | BNum :> nat -> binder. 2 | Declare Scope binder_scope. 3 | Notation "<>" := BAnon : binder_scope. 4 | 5 | Open Scope binder_scope. 6 | Lemma test : <> = <>. 7 | Proof. reflexivity. Qed. -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_obligation.v: -------------------------------------------------------------------------------- 1 | Require Import Utils. 2 | 3 | Ltac dummy_tactic n e := destruct n; try reflexivity; inversion e. 4 | 5 | Module TestObligations. 6 | 7 | Global Program Definition id4 (n : nat) : { x : nat | x = n } := 8 | if dec (Nat.leb n 0) then 0%nat 9 | else S (pred n). 10 | Local Program Definition id3 (n : nat) : { x : nat | x = n } := 11 | if dec (Nat.leb n 0) then 0%nat 12 | else S (pred n). 13 | #[global, program] 14 | Definition id2 (n : nat) : { x : nat | x = n } := 15 | if dec (Nat.leb n 0) then 0%nat 16 | else S (pred n). 17 | #[local, program] 18 | Definition id1 (n : nat) : { x : nat | x = n } := 19 | if dec (Nat.leb n 0) then 0%nat 20 | else S (pred n). 21 | 22 | Obligation 2 of id2. 23 | dummy_tactic n e. 24 | Qed. 25 | Next Obligation of id2. 26 | dummy_tactic n e. 27 | Qed. 28 | Obligation 2 of id3 : type with reflexivity. 29 | dummy_tactic n e. 30 | Qed. 31 | Next Obligation of id3 with reflexivity. 32 | dummy_tactic n e. 33 | Qed. 34 | Next Obligation. 35 | dummy_tactic n e. 36 | Qed. 37 | Next Obligation with reflexivity. 38 | dummy_tactic n e. 39 | Qed. 40 | Obligation 1. 41 | dummy_tactic n e. 42 | Qed. 43 | Obligation 2 : type with reflexivity. 44 | dummy_tactic n e. 45 | Qed. 46 | 47 | End TestObligations. 48 | 49 | Module Out. 50 | 51 | Program Definition id (n : nat) : { x : nat | x = n } := 52 | if dec (Nat.leb n 0) then 0%nat 53 | else S (pred n). 54 | 55 | Program Lemma id_lemma (n : nat) : id n = n. 56 | Proof. destruct n; try reflexivity. Qed. 57 | Program Theorem id_theorem (n : nat) : id n = n. 58 | Proof. destruct n; try reflexivity. Qed. 59 | 60 | Module In. 61 | #[program] 62 | Definition id (n : nat) : { x : nat | x = n } := 63 | if dec (Nat.leb n 0) then 0%nat 64 | else pred (S n). 65 | Obligation 1 of id with reflexivity. 66 | dummy_tactic n e. 67 | Qed. 68 | End In. 69 | 70 | Obligation 1 of id : type. 71 | dummy_tactic n e. 72 | Qed. 73 | Obligation 2 : type. 74 | dummy_tactic n e. 75 | Qed. 76 | 77 | End Out. -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_proof_cmd.v: -------------------------------------------------------------------------------- 1 | Require Import Coq.Unicode.Utf8. 2 | 3 | Section Random. 4 | Variable (n : nat) (Hn : n <> 0). 5 | 6 | Definition x := 1. 7 | 8 | Goal ∃ (m : nat), 9 | S m = n. 10 | Proof using Hn. 11 | destruct n. 12 | + exfalso. auto. 13 | + eexists. auto. 14 | Defined. 15 | 16 | Goal ∃ (m : nat), 17 | S m = n. 18 | Proof with auto. 19 | destruct n. 20 | + exfalso... 21 | + eexists... 22 | Qed. 23 | 24 | Goal nat. 25 | Proof 0. 26 | End Random. -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_section_terms.v: -------------------------------------------------------------------------------- 1 | Section Random. 2 | Let ignored : nat. 3 | Proof. exact 0. Defined. 4 | End Random. -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_simple_file.v: -------------------------------------------------------------------------------- 1 | Example test1: 1 + 1 = 2. reflexivity. Qed. 2 | 3 | Example test2: 1 + 1 + 1 = 3. rewrite test1. reflexivity. Qed. -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_theorem_tokens.v: -------------------------------------------------------------------------------- 1 | Remark test: forall x: nat, x = x. 2 | Proof. 3 | auto. 4 | Qed. 5 | 6 | Fact test2: forall x: nat, x = x. 7 | Proof. 8 | auto. 9 | Qed. 10 | 11 | Corollary test3: forall x: nat, x = x. 12 | Proof. 13 | auto. 14 | Qed. 15 | 16 | Proposition test4: forall x: nat, x = x. 17 | Proof. 18 | auto. 19 | Qed. 20 | 21 | Property test5: forall x: nat, x = x. 22 | Proof. 23 | auto. 24 | Qed. 25 | 26 | Theorem test6: forall x: nat, x = x. 27 | Proof. 28 | auto. 29 | Qed. 30 | 31 | Lemma test7: forall x: nat, x = x. 32 | Proof. 33 | auto. 34 | Qed. -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_type_class.v: -------------------------------------------------------------------------------- 1 | Module TypeClass. 2 | Class EqDecNew (A : Type) := 3 | { eqb_new : A -> A -> bool ; 4 | eqb_leibniz_new : forall x y, eqb_new x y = true -> x = y ; 5 | eqb_ident_new : forall x, eqb_new x x = true }. 6 | End TypeClass. 7 | 8 | #[refine] Global Instance unit_EqDec : TypeClass.EqDecNew unit := { eqb_new x y := true }. 9 | Proof. 10 | intros [] []; reflexivity. 11 | intros []; reflexivity. 12 | Defined. 13 | 14 | Section test. 15 | 16 | Instance test : TypeClass.EqDecNew unit -> TypeClass.EqDecNew unit. 17 | Proof. 18 | auto. 19 | Defined. 20 | 21 | End test. -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_unknown_notation.v: -------------------------------------------------------------------------------- 1 | (** * Ordering characters *) 2 | From Coq Require Import Ascii Orders OrderedType. 3 | 4 | Definition bool_compare_cont (b1 b2: bool) (k: comparison) : comparison := 5 | match b1, b2 with 6 | | false, true => Lt 7 | | true, false => Gt 8 | | _, _ => k 9 | end. 10 | 11 | Definition ascii_compare (x y: ascii) : comparison := 12 | match x, y with 13 | | Ascii x1 x2 x3 x4 x5 x6 x7 x8, Ascii y1 y2 y3 y4 y5 y6 y7 y8 => 14 | bool_compare_cont x8 y8 ( 15 | bool_compare_cont x7 y7 ( 16 | bool_compare_cont x6 y6 ( 17 | bool_compare_cont x5 y5 ( 18 | bool_compare_cont x4 y4 ( 19 | bool_compare_cont x3 y3 ( 20 | bool_compare_cont x2 y2 ( 21 | bool_compare_cont x1 y1 Eq))))))) 22 | end. 23 | 24 | (** Alternate presentation, using recursion over bitvectors. *) 25 | 26 | Fixpoint bitvect (n: nat) : Type := 27 | match n with O => bool | S n => (bitvect n * bool)%type end. 28 | 29 | Fixpoint bitvect_compare (n: nat) : bitvect n -> bitvect n -> comparison := 30 | match n with 31 | | O => (fun b1 b2 => bool_compare_cont b1 b2 Eq) 32 | | S n => (fun v1 v2 => bool_compare_cont (snd v1) (snd v2) (bitvect_compare n (fst v1) (fst v2))) 33 | end. 34 | 35 | Lemma ascii_bitvect_compare: 36 | forall x y, 37 | ascii_compare x y = 38 | match x, y with 39 | | Ascii x1 x2 x3 x4 x5 x6 x7 x8, Ascii y1 y2 y3 y4 y5 y6 y7 y8 => 40 | bitvect_compare 7%nat 41 | (x1, x2, x3, x4, x5, x6, x7, x8) 42 | (y1, y2, y3, y4, y5, y6, y7, y8) 43 | end. 44 | Proof. 45 | destruct x, y; reflexivity. 46 | Qed. 47 | 48 | Lemma bitvect_compare_refl: 49 | forall n x, bitvect_compare n x x = Eq. 50 | Proof. 51 | induction n; simpl. 52 | - destruct x; auto. 53 | - intros [x x1]; simpl. rewrite IHn. destruct x1; auto. 54 | Qed. 55 | 56 | Lemma bitvect_compare_eq: 57 | forall n x y, bitvect_compare n x y = Eq -> x = y. 58 | Proof. 59 | induction n; simpl. 60 | - destruct x, y; simpl; congruence. 61 | - intros [x x1] [y y1]; unfold bool_compare_cont; simpl; intros. 62 | destruct x1, y1; try discriminate; f_equal; eauto. 63 | Qed. 64 | 65 | Lemma bitvect_compare_lt_trans: 66 | forall n x y z, bitvect_compare n x y = Lt -> bitvect_compare n y z = Lt -> bitvect_compare n x z = Lt. 67 | Proof. 68 | induction n; simpl. 69 | - intros. destruct x, y; try discriminate. destruct z; discriminate. 70 | - intros [x x1] [y y1] [z z1]; simpl; intros. 71 | assert (A: forall b1 b2 k, bool_compare_cont b1 b2 k = Lt -> b1 = false /\ b2 = true \/ b1 = b2 /\ k = Lt). 72 | { intros. destruct b1, b2; auto; discriminate. } 73 | apply A in H. apply A in H0. 74 | destruct H as [[P1 P2] | [P1 P2]]; destruct H0 as [[Q1 Q2] | [Q1 Q2]]; subst; subst; auto. 75 | erewrite IHn by eauto. destruct z1; auto. 76 | Qed. 77 | 78 | Lemma bitvect_compare_antisym: 79 | forall n x y, CompOpp (bitvect_compare n x y) = bitvect_compare n y x. 80 | Proof. 81 | assert (A: forall b1 b2 k, CompOpp (bool_compare_cont b1 b2 k) = bool_compare_cont b2 b1 (CompOpp k)). 82 | { intros. destruct b1, b2; auto. } 83 | induction n; simpl. 84 | - destruct x, y; auto. 85 | - intros [x x1] [y y1]. simpl. rewrite A. f_equal; auto. 86 | Qed. 87 | 88 | Lemma ascii_compare_refl: 89 | forall x, ascii_compare x x = Eq. 90 | Proof. 91 | intros. rewrite ascii_bitvect_compare. destruct x. apply bitvect_compare_refl. 92 | Qed. 93 | 94 | Lemma ascii_compare_eq: 95 | forall x y, ascii_compare x y = Eq -> x = y. 96 | Proof. 97 | intros. rewrite ascii_bitvect_compare in H. destruct x, y. 98 | apply bitvect_compare_eq in H. congruence. 99 | Qed. 100 | 101 | Lemma ascii_compare_lt_trans: 102 | forall x y z, ascii_compare x y = Lt -> ascii_compare y z = Lt -> ascii_compare x z = Lt. 103 | Proof. 104 | intros. rewrite ascii_bitvect_compare in *. destruct x, y, z. 105 | eapply bitvect_compare_lt_trans; eauto. 106 | Qed. 107 | 108 | Lemma ascii_compare_antisym: 109 | forall x y, CompOpp (ascii_compare x y) = ascii_compare y x. 110 | Proof. 111 | intros. rewrite ! ascii_bitvect_compare. destruct x, y. 112 | apply bitvect_compare_antisym. 113 | Qed. 114 | 115 | (** Implementing the [OrderedType] interface *) 116 | 117 | Module OrderedAscii <: OrderedType. 118 | 119 | Definition t := ascii. 120 | Definition eq (x y: t) := x = y. 121 | Definition lt (x y: t) := ascii_compare x y = Lt. 122 | 123 | Lemma eq_refl : forall x : t, eq x x. 124 | Proof (@eq_refl t). 125 | Lemma eq_sym : forall x y : t, eq x y -> eq y x. 126 | Proof (@eq_sym t). 127 | Lemma eq_trans : forall x y z : t, eq x y -> eq y z -> eq x z. 128 | Proof (@eq_trans t). 129 | 130 | Lemma lt_trans : forall x y z : t, lt x y -> lt y z -> lt x z. 131 | Proof ascii_compare_lt_trans. 132 | 133 | Lemma lt_not_eq : forall x y : t, lt x y -> ~ eq x y. 134 | Proof. 135 | unfold lt, eq; intros; red; intros. subst y. 136 | rewrite ascii_compare_refl in H. discriminate. 137 | Qed. 138 | 139 | Definition compare (x y : t) : Compare lt eq x y. 140 | Proof. 141 | destruct (ascii_compare x y) eqn:AC. 142 | - apply EQ. apply ascii_compare_eq; auto. 143 | - apply LT. assumption. 144 | - apply GT. red. rewrite <- ascii_compare_antisym. rewrite AC; auto. 145 | Defined. 146 | 147 | Definition eq_dec (x y : t) : {x = y} + {x <> y}. 148 | Proof. 149 | destruct (ascii_compare x y) eqn:AC. 150 | - left. apply ascii_compare_eq; auto. 151 | - right; red; intros; subst y. rewrite ascii_compare_refl in AC; discriminate. 152 | - right; red; intros; subst y. rewrite ascii_compare_refl in AC; discriminate. 153 | Defined. 154 | 155 | End OrderedAscii. 156 | 157 | -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_valid.v: -------------------------------------------------------------------------------- 1 | (* Start of test file. *) 2 | 3 | Require Import Coq.Unicode.Utf8. 4 | 5 | Ltac reduce_eq := simpl; reflexivity. 6 | 7 | Module Out. 8 | Module In. 9 | Theorem plus_O_n : forall n:nat, 0 + n = n. 10 | Proof. 11 | intros n. 12 | Print plus. 13 | Print Nat.add. 14 | reduce_eq. 15 | Qed. 16 | End In. 17 | End Out. 18 | 19 | Section Random. 20 | Definition mult_0_plus : ∀ n m : nat, 21 | 0 + (S n * m) = S n * m. 22 | Proof. 23 | intros n m. 24 | rewrite -> (plus_O_n (S n * m)). 25 | Compute True /\ True. 26 | reflexivity. 27 | Abort. 28 | End Random. 29 | 30 | Module Extra. 31 | Module Fst. 32 | Record example := mk_example { fst : nat; snd : nat }. 33 | 34 | Theorem plus_O_n : forall n:nat, n = 0 + n. 35 | intros n. 36 | Compute mk_example n n. 37 | Compute Out.In.plus_O_n. 38 | reduce_eq. 39 | Defined. 40 | End Fst. 41 | 42 | Module Snd. 43 | Notation "| a |" := (S a) (at level 30, right associativity). 44 | 45 | Theorem mult_0_plus : ∀ n m : nat, 46 | S n * m = 0 + (S n * m). 47 | Proof. 48 | intros n m. 49 | rewrite <- (Fst.plus_O_n (|n| * m)). 50 | Compute {| Fst.fst := n; Fst.snd := n |}. 51 | reflexivity. 52 | Admitted. 53 | End Snd. 54 | End Extra. 55 | 56 | (* End of test file. *) -------------------------------------------------------------------------------- /coqpyt/tests/resources/test_where_notation.v: -------------------------------------------------------------------------------- 1 | Reserved Notation "A & B" (at level 80). 2 | Reserved Notation "'ONE'" (at level 80). 3 | Reserved Notation "x 🀄 y" (at level 80). 4 | 5 | Fixpoint plus_test (n m : nat) {struct n} : nat := 6 | match n with 7 | | O => m 8 | | S p => S (p + m) 9 | end 10 | where "n + m" := (plus n m) : test_scope and "n - m" := (minus n m). 11 | 12 | Inductive and' (A B : Prop) : Prop := conj' : A -> B -> A & B 13 | where "A & B" := (and' A B). 14 | 15 | Fixpoint incr (n : nat) : nat := n + ONE 16 | where "'ONE'" := 1. 17 | 18 | Fixpoint unicode x y := x 🀄 y 19 | where "x 🀄 y" := (plus_test x y). -------------------------------------------------------------------------------- /coqpyt/tests/test_cache.py: -------------------------------------------------------------------------------- 1 | from coqpyt.coq.proof_file import _AuxFile, ProofFile 2 | 3 | 4 | def test_set_cache_size(): 5 | _AuxFile.set_cache_size(256) 6 | _AuxFile._AuxFile__load_library.cache_info().maxsize == 256 7 | _AuxFile.set_cache_size(512) 8 | _AuxFile._AuxFile__load_library.cache_info().maxsize == 512 9 | ProofFile.set_library_cache_size(256) 10 | _AuxFile._AuxFile__load_library.cache_info().maxsize == 256 11 | -------------------------------------------------------------------------------- /coqpyt/tests/test_context.py: -------------------------------------------------------------------------------- 1 | from coqpyt.coq.context import FileContext 2 | from coqpyt.coq.structs import Term, TermType, Step 3 | 4 | 5 | def test_notation_colon_problem(): 6 | context = FileContext("mock.v") 7 | mock_context = { 8 | "x : y : type_scope": Term( 9 | Step("XXX", "YYY", None), TermType.NOTATION, "mock.v", [] 10 | ) 11 | } 12 | context.update(mock_context) 13 | term = context.get_notation("_ : _", "") 14 | assert term == mock_context["x : y : type_scope"] 15 | 16 | 17 | def test_notation_unscoped(): 18 | context = FileContext("mock.v") 19 | mock_context = { 20 | "x : y": Term(Step("XXX", "YYY", None), TermType.NOTATION, "mock.v", []), 21 | "z : y : test_scope": Term( 22 | Step("ZZZ", "WWW", None), TermType.NOTATION, "mock.v", [] 23 | ), 24 | } 25 | context.update(mock_context) 26 | 27 | term = context.get_notation("_ : _", "type_scope") 28 | assert term == mock_context["x : y"] 29 | 30 | term = context.get_notation("_ : _", "") 31 | assert term == mock_context["x : y"] 32 | 33 | term = context.get_notation("_ : _", "test_scope") 34 | assert term == mock_context["z : y : test_scope"] 35 | 36 | 37 | def test_notation_spaces(): 38 | context = FileContext("mock.v") 39 | mock_context = { 40 | " x + y : test_scope": Term( 41 | Step("XXX", "YYY", None), TermType.NOTATION, "mock.v", [] 42 | ), 43 | "x - y : test_scope": Term( 44 | Step("XXX", "YYY", None), TermType.NOTATION, "mock.v", [] 45 | ), 46 | } 47 | context.update(mock_context) 48 | 49 | term = context.get_notation("_ + _", "test_scope") 50 | assert term == mock_context[" x + y : test_scope"] 51 | 52 | term = context.get_notation(" _ + _ ", "test_scope") 53 | assert term == mock_context["x - y : test_scope"] 54 | -------------------------------------------------------------------------------- /coqpyt/tests/test_coq_file.py: -------------------------------------------------------------------------------- 1 | import os 2 | import uuid 3 | import shutil 4 | import pytest 5 | import tempfile 6 | 7 | from coqpyt.coq.exceptions import * 8 | from coqpyt.coq.changes import * 9 | from coqpyt.coq.base_file import CoqFile 10 | 11 | coq_file: CoqFile = None 12 | 13 | 14 | @pytest.fixture 15 | def setup(request): 16 | global coq_file 17 | file_path = os.path.join("tests/resources", request.param) 18 | new_file_path = os.path.join( 19 | tempfile.gettempdir(), 20 | "test" + str(uuid.uuid4()).replace("-", "") + ".v", 21 | ) 22 | shutil.copyfile(file_path, new_file_path) 23 | coq_file = CoqFile(new_file_path, timeout=60) 24 | yield 25 | 26 | 27 | @pytest.fixture 28 | def teardown(): 29 | yield 30 | coq_file.close() 31 | os.remove(coq_file.path) 32 | 33 | 34 | @pytest.mark.parametrize("setup", ["test_valid.v"], indirect=True) 35 | def test_is_valid(setup, teardown): 36 | assert coq_file.is_valid 37 | 38 | 39 | @pytest.mark.parametrize("setup", ["test_valid.v"], indirect=True) 40 | def test_negative_step(setup, teardown): 41 | steps = coq_file.exec(nsteps=8) 42 | assert steps[-1].text == "\n Print plus." 43 | steps = coq_file.exec(nsteps=-1) 44 | assert steps[0].text == "\n Print plus." 45 | 46 | assert "Out.In.plus_O_n" in coq_file.context.terms 47 | steps = coq_file.exec(nsteps=-3) 48 | assert steps[0].text == "\n Theorem plus_O_n : forall n:nat, 0 + n = n." 49 | assert "Out.In.plus_O_n" not in coq_file.context.terms 50 | 51 | assert coq_file.context.curr_modules == ["Out", "In"] 52 | steps = coq_file.exec(nsteps=-1) 53 | assert steps[0].text == "\n Module In." 54 | assert coq_file.context.curr_modules == ["Out"] 55 | 56 | 57 | @pytest.mark.parametrize("setup", ["test_valid.v"], indirect=True) 58 | def test_delete_step(setup, teardown): 59 | assert coq_file.steps[8].text == "\n Print Nat.add." 60 | assert coq_file.steps[8].ast.range.start.line == 12 61 | 62 | steps = coq_file.exec(nsteps=10) 63 | assert steps[-1].text == "\n reduce_eq." 64 | 65 | coq_file.delete_step(7) 66 | assert coq_file.steps[7].text == "\n Print Nat.add." 67 | assert coq_file.steps[7].ast.range.start.line == 11 68 | 69 | steps = coq_file.exec(nsteps=1) 70 | assert steps[-1].text == "\n Qed." 71 | 72 | with open(coq_file.path, "r") as f: 73 | assert "Print plus." not in f.read() 74 | 75 | 76 | @pytest.mark.parametrize("setup", ["test_valid.v"], indirect=True) 77 | def test_add_step(setup, teardown): 78 | assert coq_file.steps[8].text == "\n Print Nat.add." 79 | assert coq_file.steps[8].ast.range.start.line == 12 80 | 81 | steps = coq_file.exec(nsteps=8) 82 | assert steps[-1].text == "\n Print plus." 83 | steps_taken = coq_file.steps_taken 84 | 85 | coq_file.add_step(7, "\n Print minus.") 86 | assert coq_file.steps_taken == steps_taken 87 | steps = coq_file.exec(nsteps=1) 88 | steps_taken = coq_file.steps_taken 89 | assert steps[-1].text == "\n Print minus." 90 | 91 | coq_file.add_step(6, "\n Print minus.") 92 | assert coq_file.steps_taken == steps_taken + 1 93 | steps = coq_file.exec(nsteps=1) 94 | assert steps[-1].text == "\n Print Nat.add." 95 | assert steps[-1].ast.range.start.line == 14 96 | 97 | 98 | @pytest.mark.parametrize("setup", ["test_valid.v"], indirect=True) 99 | def test_add_definition(setup, teardown): 100 | coq_file.exec(5) 101 | steps_taken = coq_file.steps_taken 102 | 103 | assert "x" not in coq_file.context.terms 104 | coq_file.add_step(0, "\nDefinition x := 0.") 105 | assert "x" in coq_file.context.terms 106 | assert coq_file.context.get_term("x").text == "Definition x := 0." 107 | assert coq_file.steps_taken == steps_taken + 1 108 | 109 | 110 | @pytest.mark.parametrize("setup", ["test_valid.v"], indirect=True) 111 | def test_change_steps(setup, teardown): 112 | assert coq_file.steps[8].text == "\n Print Nat.add." 113 | assert coq_file.steps[8].ast.range.start.line == 12 114 | 115 | changes = [ 116 | CoqAdd("\n Print minus.", 7), 117 | CoqAdd("\n Print minus.", 6), 118 | CoqDelete(9), # Delete first print minus 119 | CoqDelete(19), # Delete Compute True /\ True. 120 | ] 121 | coq_file.change_steps(changes) 122 | steps = coq_file.exec(nsteps=8) 123 | assert steps[-1].text == "\n Print minus." 124 | assert steps[-1].ast.range.start.line == 11 125 | steps = coq_file.exec(nsteps=1) 126 | assert steps[-1].text == "\n Print plus." 127 | assert coq_file.steps[8].ast.range.start.line == 12 128 | steps = coq_file.exec(nsteps=11) 129 | assert steps[-1].text == "\n reflexivity." 130 | 131 | with pytest.raises(InvalidChangeException): 132 | coq_file.change_steps( 133 | [ 134 | CoqAdd("\n Print minus.", 7), 135 | CoqDelete(11), # delete reduce_eq 136 | ] 137 | ) 138 | 139 | 140 | @pytest.mark.parametrize("setup", ["test_valid.v"], indirect=True) 141 | def test_add_proof(setup, teardown): 142 | coq_file.run() 143 | steps_taken = coq_file.steps_taken 144 | assert "change_steps" not in coq_file.context.terms 145 | 146 | coq_file.change_steps( 147 | [ 148 | CoqAdd(" Defined.", 12), 149 | CoqAdd("\n reflexivity.", 12), 150 | CoqAdd("\n rewrite -> (plus_O_n (S n * m)).", 12), 151 | # Checks if there aren't problems with intermediate states 152 | # (e.g. the ranges of the AST are updated incorrectly) 153 | CoqDelete(13), 154 | CoqAdd("\n intros n m.", 12), 155 | CoqAdd("\nProof.", 12), 156 | CoqAdd( 157 | "\nDefinition change_steps : ∀ n m : nat,\n 0 + (S n * m) = S n * m.", 158 | 12, 159 | ), 160 | ] 161 | ) 162 | 163 | assert "change_steps" in coq_file.context.terms 164 | assert coq_file.steps_taken == steps_taken + 5 165 | 166 | 167 | @pytest.mark.parametrize("setup", ["test_valid.v"], indirect=True) 168 | def test_delete_proof(setup, teardown): 169 | # Test if mult_0_plus is removed 170 | # It also tests if deletion with invalid intermediate states works 171 | coq_file.run() 172 | steps_taken = coq_file.steps_taken 173 | assert "mult_0_plus" in coq_file.context.terms 174 | coq_file.change_steps([CoqDelete(14) for _ in range(7)]) 175 | assert "mult_0_plus" not in coq_file.context.terms 176 | assert coq_file.steps_taken == steps_taken - 7 177 | 178 | 179 | @pytest.mark.parametrize("setup", ["test_where_notation.v"], indirect=True) 180 | def test_where_notation(setup, teardown): 181 | coq_file.run() 182 | assert "n + m : test_scope" in coq_file.context.terms 183 | assert ( 184 | coq_file.context.terms["n + m : test_scope"].text 185 | == 'Fixpoint plus_test (n m : nat) {struct n} : nat := match n with | O => m | S p => S (p + m) end where "n + m" := (plus n m) : test_scope and "n - m" := (minus n m).' 186 | ) 187 | assert "n - m" in coq_file.context.terms 188 | assert ( 189 | coq_file.context.terms["n - m"].text 190 | == 'Fixpoint plus_test (n m : nat) {struct n} : nat := match n with | O => m | S p => S (p + m) end where "n + m" := (plus n m) : test_scope and "n - m" := (minus n m).' 191 | ) 192 | assert "A & B" in coq_file.context.terms 193 | assert ( 194 | coq_file.context.terms["A & B"].text 195 | == "Inductive and' (A B : Prop) : Prop := conj' : A -> B -> A & B where \"A & B\" := (and' A B)." 196 | ) 197 | assert "'ONE'" in coq_file.context.terms 198 | assert ( 199 | coq_file.context.terms["'ONE'"].text 200 | == "Fixpoint incr (n : nat) : nat := n + ONE where \"'ONE'\" := 1." 201 | ) 202 | assert "x 🀄 y" in coq_file.context.terms 203 | assert ( 204 | coq_file.context.terms["x 🀄 y"].text 205 | == 'Fixpoint unicode x y := x 🀄 y where "x 🀄 y" := (plus_test x y).' 206 | ) 207 | 208 | 209 | @pytest.mark.parametrize("setup", ["test_get_notation.v"], indirect=True) 210 | def test_get_notation(setup, teardown): 211 | coq_file.run() 212 | assert ( 213 | coq_file.context.get_notation("'_' _ '_' _ '_'", "test_scope").text 214 | == "Notation \"'_' AB '_' BC '_'\" := (plus AB BC) : test_scope." 215 | ) 216 | assert ( 217 | coq_file.context.get_notation("'C_D' _ 'C_D'", "test_scope").text 218 | == "Notation \"'C_D' A_B 'C_D'\" := (plus A_B A_B) : test_scope." 219 | ) 220 | assert ( 221 | coq_file.context.get_notation("_ ++ _", "list_scope").text 222 | == 'Infix "++" := app (right associativity, at level 60) : list_scope.' 223 | ) 224 | 225 | 226 | @pytest.mark.parametrize("setup", ["test_invalid_1.v"], indirect=True) 227 | def test_is_invalid_1(setup, teardown): 228 | assert not coq_file.is_valid 229 | steps = coq_file.run() 230 | assert len(steps[11].diagnostics) == 1 231 | assert ( 232 | steps[11].diagnostics[0].message 233 | == 'Found no subterm matching "0 + ?M152" in the current goal.' 234 | ) 235 | assert steps[11].diagnostics[0].severity == 1 236 | 237 | 238 | @pytest.mark.parametrize("setup", ["test_invalid_2.v"], indirect=True) 239 | def test_is_invalid_2(setup, teardown): 240 | assert not coq_file.is_valid 241 | steps = coq_file.run() 242 | assert len(steps[15].diagnostics) == 1 243 | assert ( 244 | steps[15].diagnostics[0].message 245 | == "Syntax error: '.' expected after [command] (in [vernac_aux])." 246 | ) 247 | assert steps[15].diagnostics[0].severity == 1 248 | 249 | 250 | @pytest.mark.parametrize("setup", ["test_module_type.v"], indirect=True) 251 | def test_module_type(setup, teardown): 252 | coq_file.run() 253 | # We ignore terms inside a Module Type since they can't be used outside 254 | # and should be overriden. 255 | assert len(coq_file.context.terms) == 1 256 | assert "plus_O_n" in coq_file.context.terms 257 | 258 | 259 | @pytest.mark.parametrize("setup", ["test_derive.v"], indirect=True) 260 | def test_derive(setup, teardown): 261 | coq_file.run() 262 | for key in ["incr", "incr_correct"]: 263 | assert key in coq_file.context.terms 264 | assert ( 265 | coq_file.context.terms[key].text 266 | == "Derive incr SuchThat (forall n, incr n = plus 1 n) As incr_correct." 267 | ) 268 | keywords = [ 269 | "Inversion", 270 | "Inversion_clear", 271 | "Dependent Inversion", 272 | "Dependent Inversion_clear", 273 | ] 274 | for i in range(4): 275 | key = f"leminv{i + 1}" 276 | assert key in coq_file.context.terms 277 | assert ( 278 | coq_file.context.terms[key].text 279 | == f"Derive {keywords[i]} {key} with (forall n m:nat, Le (S n) m) Sort Prop." 280 | ) 281 | 282 | 283 | @pytest.mark.extra 284 | @pytest.mark.parametrize("setup", ["test_equations.v"], indirect=True) 285 | def test_equations(setup, teardown): 286 | coq_file.run() 287 | assert len(coq_file.context.terms) == 0 288 | assert coq_file.context.last_term is not None 289 | assert ( 290 | coq_file.context.last_term.text 291 | == "Equations? f (n : nat) : nat := f 0 := 42 ; f (S m) with f m := { f (S m) IH := _ }." 292 | ) 293 | 294 | 295 | def test_space_in_path(): 296 | # This test exists because coq-lsp encodes spaces in paths as %20 297 | # This causes the diagnostics to be saved in a different path than the one 298 | # considered by CoqPyt. This was fixed by unquoting the path given 299 | # by coq-lsp. 300 | with CoqFile("tests/resources/test test/test_error.v") as coq_file: 301 | assert not coq_file.is_valid 302 | 303 | 304 | @pytest.mark.parametrize("setup", ["test_simple_file.v"], indirect=True) 305 | def test_diagnostics(setup, teardown): 306 | coq_file.run() 307 | assert len(coq_file.diagnostics) == 2 308 | assert len(coq_file.errors) == 0 309 | 310 | with pytest.raises(InvalidAddException) as e: 311 | coq_file.add_step(0, "\n Qed.") 312 | assert len(e.value.errors) == 3 313 | 314 | assert len(coq_file.diagnostics) == 2 315 | assert len(coq_file.errors) == 0 316 | 317 | 318 | @pytest.mark.parametrize("setup", ["test_invalid_1.v"], indirect=True) 319 | def test_diagnostics_invalid(setup, teardown): 320 | coq_file.run() 321 | assert len(coq_file.diagnostics) == 7 322 | assert len(coq_file.errors) == 1 323 | assert ( 324 | coq_file.errors[0].message 325 | == 'Found no subterm matching "0 + ?M152" in the current goal.' 326 | ) 327 | -------------------------------------------------------------------------------- /coqpyt/tests/test_coq_lsp_client.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from coqpyt.lsp.structs import * 4 | from coqpyt.coq.lsp.client import CoqLspClient 5 | 6 | 7 | def test_save_vo(): 8 | client = CoqLspClient("tests/resources") 9 | file_path = f"{os.getcwd()}/tests/resources/test_valid.v" 10 | uri = f"file://{os.getcwd()}/tests/resources/test_valid.v" 11 | with open(file_path, "r") as f: 12 | client.didOpen(TextDocumentItem(uri, "coq", 1, f.read())) 13 | versionId = TextDocumentIdentifier(uri) 14 | client.save_vo(versionId) 15 | client.shutdown() 16 | client.exit() 17 | assert os.path.exists("tests/resources/test_valid.vo") 18 | os.remove("tests/resources/test_valid.vo") 19 | -------------------------------------------------------------------------------- /coqpyt/tests/test_json_rpc_endpoint.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | 4 | from coqpyt import lsp 5 | 6 | JSON_RPC_RESULT_LIST = [ 7 | 'Content-Length: 40\r\n\r\n{"key_str": "some_string", "key_num": 1}'.encode( 8 | "utf-8" 9 | ), 10 | 'Content-Length: 40\r\n\r\n{"key_num": 1, "key_str": "some_string"}'.encode( 11 | "utf-8" 12 | ), 13 | ] 14 | 15 | 16 | def test_send_sanity(): 17 | pipein, pipeout = os.pipe() 18 | pipein = os.fdopen(pipein, "rb") 19 | pipeout = os.fdopen(pipeout, "wb") 20 | json_rpc_endpoint = lsp.JsonRpcEndpoint(pipeout, None) 21 | json_rpc_endpoint.send_request({"key_num": 1, "key_str": "some_string"}) 22 | result = pipein.read(len(JSON_RPC_RESULT_LIST[0])) 23 | assert result in JSON_RPC_RESULT_LIST 24 | 25 | 26 | def test_send_class(): 27 | class RpcClass(object): 28 | def __init__(self, value_num, value_str): 29 | self.key_num = value_num 30 | self.key_str = value_str 31 | 32 | pipein, pipeout = os.pipe() 33 | pipein = os.fdopen(pipein, "rb") 34 | pipeout = os.fdopen(pipeout, "wb") 35 | json_rpc_endpoint = lsp.JsonRpcEndpoint(pipeout, None) 36 | json_rpc_endpoint.send_request(RpcClass(1, "some_string")) 37 | result = pipein.read(len(JSON_RPC_RESULT_LIST[0])) 38 | assert result in JSON_RPC_RESULT_LIST 39 | 40 | 41 | def test_recv_sanity(): 42 | pipein, pipeout = os.pipe() 43 | pipein = os.fdopen(pipein, "rb") 44 | pipeout = os.fdopen(pipeout, "wb") 45 | json_rpc_endpoint = lsp.JsonRpcEndpoint(None, pipein) 46 | pipeout.write( 47 | 'Content-Length: 40\r\n\r\n{"key_str": "some_string", "key_num": 1}'.encode( 48 | "utf-8" 49 | ) 50 | ) 51 | pipeout.flush() 52 | result = json_rpc_endpoint.recv_response() 53 | assert {"key_num": 1, "key_str": "some_string"} == result 54 | 55 | 56 | def test_recv_wrong_header(): 57 | pipein, pipeout = os.pipe() 58 | pipein = os.fdopen(pipein, "rb") 59 | pipeout = os.fdopen(pipeout, "wb") 60 | json_rpc_endpoint = lsp.JsonRpcEndpoint(None, pipein) 61 | pipeout.write( 62 | 'Contentength: 40\r\n\r\n{"key_str": "some_string", "key_num": 1}'.encode( 63 | "utf-8" 64 | ) 65 | ) 66 | pipeout.flush() 67 | with pytest.raises(lsp.structs.ResponseError): 68 | result = json_rpc_endpoint.recv_response() 69 | print("should never get here", result) 70 | 71 | 72 | def test_recv_missing_size(): 73 | pipein, pipeout = os.pipe() 74 | pipein = os.fdopen(pipein, "rb") 75 | pipeout = os.fdopen(pipeout, "wb") 76 | json_rpc_endpoint = lsp.JsonRpcEndpoint(None, pipein) 77 | pipeout.write( 78 | 'Content-Type: 40\r\n\r\n{"key_str": "some_string", "key_num": 1}'.encode( 79 | "utf-8" 80 | ) 81 | ) 82 | pipeout.flush() 83 | with pytest.raises(lsp.structs.ResponseError): 84 | result = json_rpc_endpoint.recv_response() 85 | print("should never get here", result) 86 | 87 | 88 | def test_recv_close_pipe(): 89 | pipein, pipeout = os.pipe() 90 | pipein = os.fdopen(pipein, "rb") 91 | pipeout = os.fdopen(pipeout, "wb") 92 | json_rpc_endpoint = lsp.JsonRpcEndpoint(None, pipein) 93 | pipeout.close() 94 | result = json_rpc_endpoint.recv_response() 95 | assert result is None 96 | -------------------------------------------------------------------------------- /coqpyt/tests/test_readme.py: -------------------------------------------------------------------------------- 1 | import os 2 | import uuid 3 | import pytest 4 | import tempfile 5 | import subprocess 6 | 7 | temp_path = os.path.join(tempfile.gettempdir(), str(uuid.uuid4())) 8 | 9 | 10 | @pytest.fixture 11 | def teardown_aux(): 12 | yield 13 | if os.path.exists(temp_path): 14 | os.remove(temp_path) 15 | 16 | 17 | def test_readme_example(teardown_aux): 18 | readme_path = "../examples/readme" 19 | with open(f"{readme_path}.py", "r") as f: 20 | script, vfile = f.read(), f"{readme_path}.v" 21 | script = vfile.join(script.split(vfile[3:])) 22 | with open(temp_path, "w") as f2: 23 | f2.write(script) 24 | run = subprocess.run(f"python3 {temp_path}", shell=True, capture_output=True) 25 | assert run.returncode == 0 26 | -------------------------------------------------------------------------------- /examples/readme.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from coqpyt.coq.structs import TermType 4 | from coqpyt.coq.base_file import CoqFile 5 | from coqpyt.coq.proof_file import ProofFile 6 | from coqpyt.coq.changes import ProofAppend, ProofPop 7 | from coqpyt.coq.exceptions import InvalidChangeException 8 | 9 | # Open Coq file 10 | with CoqFile(os.path.join(os.getcwd(), "examples/readme.v")) as coq_file: 11 | coq_file.exec(nsteps=2) 12 | # Get all terms defined until now 13 | print("Number of terms:", len(coq_file.context.terms)) 14 | # Filter by Tactics 15 | print( 16 | "Number of tactics:", 17 | len( 18 | list( 19 | filter( 20 | lambda term: term.type == TermType.TACTIC, 21 | coq_file.context.terms.values(), 22 | ) 23 | ) 24 | ), 25 | ) 26 | 27 | # Save compiled file 28 | coq_file.save_vo() 29 | print("Compiled file exists:", os.path.exists("examples/readme.vo")) 30 | os.remove("examples/readme.vo") 31 | 32 | # Run remaining file 33 | coq_file.run() 34 | print("Checked:", coq_file.checked) 35 | # Get all terms defined until now 36 | print("Number of terms:", len(coq_file.context.terms)) 37 | 38 | # Open Proof file 39 | with ProofFile(os.path.join(os.getcwd(), "examples/readme.v")) as proof_file: 40 | # Enter proof 41 | proof_file.exec(nsteps=4) 42 | print("In proof:", proof_file.in_proof) 43 | # Get current goals 44 | print(proof_file.current_goals) 45 | 46 | # Run remaining file 47 | proof_file.run() 48 | # Number of proofs in the file 49 | print("Number of proofs:", len(proof_file.proofs)) 50 | print("Proof:", proof_file.proofs[0].text) 51 | 52 | # Print steps of proof 53 | for step in proof_file.proofs[0].steps: 54 | print(step.text, end="") 55 | print() 56 | 57 | # Get the context used in the third step 58 | print(proof_file.proofs[0].steps[2].context) 59 | # Print the goals in the third step 60 | print(proof_file.proofs[0].steps[2].goals) 61 | 62 | # Print number of terms in context 63 | print("Number of terms:", len(proof_file.context.terms)) 64 | # Filter for Notations only 65 | print( 66 | "Number of notations:", 67 | len( 68 | list( 69 | filter( 70 | lambda term: term.type == TermType.NOTATION, 71 | proof_file.context.terms.values(), 72 | ) 73 | ) 74 | ), 75 | ) 76 | 77 | 78 | def reset_proof(file: ProofFile): 79 | file.run() 80 | proven = file.proofs[1] 81 | file.pop_step(proven) 82 | file.pop_step(proven) 83 | file.pop_step(proven) 84 | file.append_step(proven, "\nAdmitted.") 85 | 86 | 87 | with ProofFile(os.path.join(os.getcwd(), "examples/readme.v")) as proof_file: 88 | proof_file.run() 89 | # Get the first admitted proof 90 | unproven = proof_file.unproven_proofs[0] 91 | # Steps for an incorrect proof 92 | incorrect = [" reflexivity.", "\nQed."] 93 | # Steps for a correct proof 94 | correct = [" rewrite app_assoc."] + incorrect 95 | 96 | # Loop through both attempts 97 | for attempt in [incorrect, correct]: 98 | # Remove the "\nAdmitted." step 99 | proof_file.pop_step(unproven) 100 | try: 101 | # Append all steps in the attempt 102 | for i, s in enumerate(attempt): 103 | proof_file.append_step(unproven, s) 104 | print("Proof succeeded!") 105 | break 106 | except InvalidChangeException: 107 | # Some step was invalid, so we rollback the previous changes 108 | [proof_file.pop_step(unproven) for _ in range(i)] 109 | proof_file.append_step(unproven, "\nAdmitted.") 110 | print("Proof attempt not valid.") 111 | reset_proof(proof_file) 112 | 113 | with ProofFile(os.path.join(os.getcwd(), "examples/readme.v")) as proof_file: 114 | proof_file.run() 115 | # Get the first admitted proof 116 | unproven = proof_file.unproven_proofs[0] 117 | # Steps for an incorrect proof 118 | incorrect = [" reflexivity.", "\nQed."] 119 | # Steps for a correct proof 120 | correct = [" rewrite app_assoc."] + incorrect 121 | 122 | # Loop through both attempts 123 | for attempt in [incorrect, correct]: 124 | # Schedule the removal of the "\nAdmitted." step 125 | changes = [ProofPop()] 126 | # Schedule the addition of each step in the attempt 127 | for s in attempt: 128 | changes.append(ProofAppend(s)) 129 | try: 130 | # Apply all changes in one batch 131 | proof_file.change_proof(unproven, changes) 132 | print("Proof succeeded!") 133 | break 134 | except InvalidChangeException: 135 | # Some batch of changes was invalid 136 | # Rollback is automatic, so no rollback needed 137 | print("Proof attempt not valid.") 138 | reset_proof(proof_file) 139 | -------------------------------------------------------------------------------- /examples/readme.v: -------------------------------------------------------------------------------- 1 | Require Import Coq.Unicode.Utf8. 2 | Require Import List. 3 | 4 | Ltac reduce_eq := simpl; reflexivity. 5 | 6 | Theorem mult_0_plus : ∀ n m : nat, 0 + (S n * m) = S n * m. 7 | Proof. 8 | intros n m. 9 | rewrite -> (plus_O_n (S n * m)). 10 | reflexivity. 11 | Qed. 12 | 13 | Lemma rev_append: forall {a} (l1 l2: list a), 14 | rev (l1 ++ l2) = rev l2 ++ rev l1. 15 | Proof. 16 | intros a l1 l2. induction l1; intros. 17 | - simpl. rewrite app_nil_r. reflexivity. 18 | - simpl. rewrite IHl1. 19 | Admitted. -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sr-lab/coqpyt/75a3cca35f6d0f4043b94c26948afde8869ebd77/images/logo.png -------------------------------------------------------------------------------- /images/uml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sr-lab/coqpyt/75a3cca35f6d0f4043b94c26948afde8869ebd77/images/uml.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pytest==7.3.1 2 | pyyaml==6.0.0 3 | packaging>=23.2 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | 4 | from setuptools import setup, find_packages 5 | from setuptools.command.test import test as TestCommand 6 | 7 | with open("README.md", "r") as fh: 8 | long_description = fh.read() 9 | 10 | 11 | class PyTest(TestCommand): 12 | user_options = [("pytest-args=", "a", "Arguments to pass to pytest")] 13 | 14 | def initialize_options(self): 15 | TestCommand.initialize_options(self) 16 | self.pytest_args = [] 17 | 18 | def run_tests(self): 19 | # import here, cause outside the eggs aren't loaded 20 | import pytest 21 | 22 | errno = pytest.main(self.pytest_args) 23 | sys.exit(errno) 24 | 25 | 26 | setup( 27 | name="coqpyt", 28 | version="0.0.1", 29 | author="Pedro Carrott, Nuno Saavedra, Avi Yeger", 30 | author_email="pedro.carrott@imperial.ac.uk", 31 | description="CoqPyt: a Python client for coq-lsp", 32 | long_description=long_description, 33 | long_description_content_type="text/markdown", 34 | url="https://github.com/sr-lab/coqpyt", 35 | packages=find_packages(), 36 | tests_require=["pytest", "pytest_mock"], 37 | cmdclass={"test": PyTest}, 38 | ) 39 | --------------------------------------------------------------------------------