Skip to content

Commit f47237a

Browse files
authored
Add support to Rocq 9.0.1 (#63)
1 parent 17e2fa6 commit f47237a

File tree

15 files changed

+440
-80
lines changed

15 files changed

+440
-80
lines changed

.github/workflows/lint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@ jobs:
1717
uses: psf/black@stable
1818
with:
1919
options: "--check --verbose"
20-
version: "23.3.0"
20+
version: "25.9.0"

.github/workflows/test.yml

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,23 @@ jobs:
1212
strategy:
1313
matrix:
1414
ocaml-compiler:
15-
- "5.2.0"
15+
- "5.3.0"
1616
coq-version:
17-
- "8.17.1"
18-
- "8.18.0"
19-
- "8.19.2"
20-
- "8.20.0"
17+
- main: "8.17"
18+
patch: "1"
19+
- main: "8.18"
20+
patch: "0"
21+
- main: "8.19"
22+
patch: "2"
23+
- main: "8.20"
24+
patch: "1"
25+
- main: "9.0"
26+
patch: "1"
27+
coq-lsp-version:
28+
- "0.2.3"
2129
python-version:
22-
- '3.11'
2330
- '3.12'
31+
- '3.13'
2432

2533
steps:
2634
- name: Checkout
@@ -42,7 +50,7 @@ jobs:
4250
with:
4351
path: |
4452
/home/runner/work/coqpyt/coqpyt/_opam/
45-
key: ${{ matrix.ocaml-compiler }}-${{ matrix.coq-version }}-opam
53+
key: ${{ matrix.ocaml-compiler }}-${{ matrix.coq-version.main }}.${{ matrix.coq-version.patch }}-opam
4654

4755
- name: Set-up OCaml
4856
uses: ocaml/setup-ocaml@v3
@@ -52,8 +60,8 @@ jobs:
5260
- name: Install coq-lsp
5361
if: steps.cache-opam-restore.outputs.cache-hit != 'true'
5462
run: |
55-
opam pin add coq ${{ matrix.coq-version }}
56-
opam install coq-lsp
63+
opam pin add coq ${{ matrix.coq-version.main }}.${{ matrix.coq-version.patch }}
64+
opam pin add coq-lsp ${{ matrix.coq-lsp-version }}+${{ matrix.coq-version.main }}
5765
5866
- name: Add coq-released
5967
if: steps.cache-opam-restore.outputs.cache-hit != 'true'

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ If you use CoqPyt in an article, please cite:
2020

2121
## Installation
2222

23-
[Python](https://www.python.org/) must be installed on version >= 3.11.
23+
[Python](https://www.python.org/) must be installed on version >= 3.12.
2424

25-
[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).
25+
[coq-lsp](https://github.com/ejgallego/coq-lsp) should be installed on version 0.2.3 (earlier versions are untested but may be compatible). Follow the installation instructions provided [here](https://github.com/ejgallego/coq-lsp#%EF%B8%8F-installation).
2626

2727
```bash
2828
pip install -r requirements.txt

coqpyt/coq/base_file.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def __exit__(self, exc_type, exc_value, traceback):
9696

9797
def __init_path(self, file_path, library):
9898
self.file_module = [] if library is None else library.split(".")
99-
self.__from_lib = self.file_module[:2] == ["Coq", "Init"]
99+
self.__from_lib = self.file_module[:2] in [["Coq", "Init"], ["Corelib", "Init"]]
100100
self.path = file_path
101101
if not self.__from_lib:
102102
self._path = file_path

coqpyt/coq/context.py

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,48 @@ def __init_coq_version(self, coqtop):
4040
# from ProofFile to here.
4141
self.ext_index = lambda e: e["ext_index"] if post18 else e[1]
4242

43-
# We only tested versions 8.17/8.18/8.19, so we provide no claims about
43+
# For versions 9.0+, the AST structure for fixpoints changed and obligation
44+
# commands have less tags
45+
rocq = version.parse(coq_version) >= version.parse("9.0")
46+
self.__fixpoint_notations = lambda e: (
47+
[]
48+
if len(e) < 3 or not isinstance(e[2], list)
49+
else (
50+
e[2][1][0]["notations"]
51+
if (
52+
rocq
53+
and len(e[2]) > 1
54+
and isinstance(e[2][1], list)
55+
and len(e[2][1]) > 0
56+
and isinstance(e[2][1][0], dict)
57+
and "notations" in e[2][1][0]
58+
and isinstance(e[2][1][0]["notations"], list)
59+
)
60+
else (
61+
e[2][0]["notations"]
62+
if (
63+
not rocq
64+
and len(e[2]) > 0
65+
and isinstance(e[2][0], dict)
66+
and "notations" in e[2][0]
67+
and isinstance(e[2][0]["notations"], list)
68+
)
69+
else []
70+
)
71+
)
72+
)
73+
# FIXME: This should be made private once [__get_program_context] is extracted
74+
# from ProofFile to here.
75+
# Tags pre-Rocq: Tags post-Rocq:
76+
# 0 - Obligation N of id : type
77+
# 1 - Obligation N of id 0 - Obligation N of id
78+
# 2 - Obligation N : type
79+
# 3 - Obligation N 1 - Obligation N
80+
# 4 - Next Obligation of id 2 - Next Obligation of id
81+
# 5 - Next Obligation 3 - Next Obligation
82+
self.obligation_tag_with_id = lambda t: t in ([0, 2] if rocq else [0, 1, 4])
83+
84+
# We only tested versions 8.17 to 9.0, so we provide no claims about
4485
# versions prior to that.
4586

4687
def __init_context(self, terms: Optional[Dict[str, Term]] = None):
@@ -188,17 +229,8 @@ def __handle_where_notations(self, step: Step, expr: List, term_type: TermType):
188229
and len(expr[2][0][1]) > 0
189230
):
190231
spans = expr[2][0][1]
191-
elif (
192-
term_type == TermType.FIXPOINT
193-
and len(expr) > 2
194-
and isinstance(expr[2], list)
195-
and len(expr[2]) > 0
196-
and isinstance(expr[2][0], dict)
197-
and "notations" in expr[2][0]
198-
and isinstance(expr[2][0]["notations"], list)
199-
and len(expr[2][0]["notations"]) > 0
200-
):
201-
spans = expr[2][0]["notations"]
232+
elif term_type == TermType.FIXPOINT:
233+
spans = self.__fixpoint_notations(expr)
202234

203235
# handles when multiple notations are defined
204236
for span in spans:

coqpyt/coq/proof_file.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -458,15 +458,8 @@ def __step_context(self, step: Step) -> List[Term]:
458458

459459
def __get_program_context(self) -> Tuple[Term, List[Term]]:
460460
expr = self.context.expr(self.prev_step)
461-
# Tags:
462-
# 0 - Obligation N of id : type
463-
# 1 - Obligation N of id
464-
# 2 - Obligation N : type
465-
# 3 - Obligation N
466-
# 4 - Next Obligation of id
467-
# 5 - Next Obligation
468461
tag = self.context.ext_index(expr[1])
469-
if tag in [0, 1, 4]:
462+
if self.context.obligation_tag_with_id(tag):
470463
stack = expr[:0:-1]
471464
while len(stack) > 0:
472465
el = stack.pop()
@@ -483,7 +476,7 @@ def __get_program_context(self) -> Tuple[Term, List[Term]]:
483476
for v in reversed(el):
484477
if isinstance(v, list):
485478
stack.append(v)
486-
elif tag in [2, 3, 5]:
479+
else:
487480
id = self.current_goals.program[0][0][1]
488481
# This works because the obligation must be in the
489482
# same module as the program
@@ -1047,3 +1040,9 @@ def change_steps(self, changes: List[CoqChange]):
10471040
def close(self):
10481041
super().close()
10491042
self.__aux_file.close()
1043+
1044+
@staticmethod
1045+
def clear_disk_cache():
1046+
cache_loc = _AuxFile.get_coqpyt_disk_cache_loc()
1047+
if cache_loc is not None and os.path.exists(cache_loc):
1048+
shutil.rmtree(cache_loc, ignore_errors=True)

coqpyt/tests/proof_file/expected/imports.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,9 @@ proofs:
173173
"8.20.x":
174174
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."
175175
type: NOTATION
176+
"9.0.x":
177+
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."
178+
type: NOTATION
176179
default:
177180
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."
178181
type: NOTATION

coqpyt/tests/proof_file/expected/valid_file.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,9 @@ proofs:
204204
"8.20.x":
205205
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."
206206
type: NOTATION
207+
"9.0.x":
208+
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."
209+
type: NOTATION
207210
default:
208211
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."
209212
type: NOTATION
@@ -373,6 +376,9 @@ proofs:
373376
"8.20.x":
374377
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."
375378
type: NOTATION
379+
"9.0.x":
380+
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."
381+
type: NOTATION
376382
default:
377383
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."
378384
type: NOTATION

coqpyt/tests/proof_file/test_cache.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ def term_files_eq(terms1: dict[str, Term], terms2: dict[str, Term]) -> bool:
2121
return True
2222

2323
def test_cache(self):
24+
ProofFile.clear_disk_cache()
25+
2426
with ProofFile(
2527
"tests/resources/test_imports/test_import.v",
2628
workspace="tests/resources/test_import",

coqpyt/tests/proof_file/test_changes.py

Lines changed: 119 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from coqpyt.coq.changes import *
66

77
from utility import *
8+
from packaging import version
89

910

1011
class TestProofValidFile(SetupProofFile):
@@ -23,12 +24,18 @@ def test_delete_and_add(self):
2324
if i != len(test_proofs["proofs"][0]["steps"]) - 1:
2425
step["goals"]["goals"]["goals"][0]["hyps"] = []
2526
step["goals"]["goals"]["goals"][0]["ty"] = "∀ n : nat, 0 + n = n"
26-
check_proof(test_proofs["proofs"][0], proof_file.proofs[0])
27+
check_proof(
28+
test_proofs["proofs"][0],
29+
proof_file.proofs[0],
30+
)
2731

2832
proof_file.add_step(5, "\n intros n.")
2933

3034
test_proofs = get_test_proofs("tests/proof_file/expected/valid_file.yml")
31-
check_proof(test_proofs["proofs"][0], proof_file.proofs[0])
35+
check_proof(
36+
test_proofs["proofs"][0],
37+
proof_file.proofs[0],
38+
)
3239

3340
# Check if context is changed correctly
3441
proof_file.add_step(7, "\n Print minus.")
@@ -53,7 +60,10 @@ def test_delete_and_add(self):
5360
test_proofs["proofs"][0]["steps"].insert(3, step)
5461
for i, step in enumerate(test_proofs["proofs"][0]["steps"][4:]):
5562
step["goals"]["position"]["line"] += 1
56-
check_proof(test_proofs["proofs"][0], proof_file.proofs[0])
63+
check_proof(
64+
test_proofs["proofs"][0],
65+
proof_file.proofs[0],
66+
)
5767

5868
# Add step in beginning of proof
5969
proof_file.add_step(26, "\n Print plus.")
@@ -115,7 +125,10 @@ def test_change_steps(self):
115125
test_proofs["proofs"][0]["steps"].insert(3, step)
116126
for step in test_proofs["proofs"][0]["steps"][4:]:
117127
step["goals"]["position"]["line"] += 1
118-
check_proof(test_proofs["proofs"][0], proof_file.proofs[0])
128+
check_proof(
129+
test_proofs["proofs"][0],
130+
proof_file.proofs[0],
131+
)
119132

120133
# Add step in beginning of proof
121134
proof_file.change_steps([CoqAdd("\n Print plus.", 26)])
@@ -558,6 +571,108 @@ class TestProofChangeObligation(SetupProofFile):
558571
def setup_method(self, method):
559572
self.setup("test_obligation.v")
560573

574+
def test_change_obligation(self):
575+
proof_file = self.proof_file
576+
577+
# Delete unwanted steps
578+
proof_file.change_steps([CoqDelete(2) for _ in range(30)])
579+
proof_file.change_steps([CoqDelete(4) for _ in range(8)])
580+
proof_file.change_steps([CoqDelete(16), CoqDelete(2)])
581+
proof_file.change_steps([CoqDelete(12) for _ in range(3)])
582+
583+
# Add a Program definition
584+
program = (
585+
"\n\nProgram Definition idx (n : nat) : { x : nat | x = n } :="
586+
+ "\n if dec (Nat.leb n 0) then 0%nat"
587+
+ "\n else pred (S n)."
588+
)
589+
proof_file.add_step(8, program)
590+
591+
# Add a Program lemma and proof
592+
lemma = "\nProgram Lemma id_lemma (n : nat) : id n = n."
593+
lemma_proof = ["\nProof.", " destruct n; try reflexivity.", " Qed."]
594+
proof_file.add_step(9, lemma)
595+
proof_file.change_steps([CoqAdd(lemma_proof[i], 10 + i) for i in range(3)])
596+
597+
# Add a proof for each obligation of the new Program
598+
proof = ["\nNext Obligation.", "\n dummy_tactic n e.", "\nQed."]
599+
for i, step in enumerate(proof):
600+
proof_file.add_step(16 + i, step)
601+
for i, step in enumerate(proof):
602+
proof_file.add_step(19 + i, step)
603+
604+
texts = [
605+
"Obligation 1 of id with reflexivity.",
606+
"Obligation 1 of id.",
607+
"Next Obligation.",
608+
"Next Obligation.",
609+
]
610+
programs = [
611+
("#[program]", "id", "pred (S n)"),
612+
("Program", "id", "S (pred n)"),
613+
("Program", "id", "S (pred n)"),
614+
("Program", "idx", "pred (S n)"),
615+
]
616+
617+
# Steps were added to the end of the file
618+
proof_file.run()
619+
620+
# Check the proofs
621+
assert len(proof_file.proofs) == 5
622+
assert len(proof_file.open_proofs) == 0
623+
proofs = proof_file.proofs[:1] + proof_file.proofs[2:]
624+
for i, proof in enumerate(proofs):
625+
assert proof.text == texts[i]
626+
assert proof.program is not None
627+
assert (
628+
proof.program.text
629+
== programs[i][0]
630+
+ " Definition "
631+
+ programs[i][1]
632+
+ " (n : nat) : { x : nat | x = n } := if dec (Nat.leb n 0) then 0%nat else "
633+
+ programs[i][2]
634+
+ "."
635+
)
636+
assert len(proof.steps) == 2
637+
assert proof.steps[0].text == "\n dummy_tactic n e."
638+
assert proof_file.proofs[1].text == lemma[1:]
639+
assert proof_file.proofs[1].program is None
640+
for i, step in enumerate(proof_file.proofs[1].steps):
641+
assert lemma_proof[i] == step.text
642+
643+
# Delete last Next Obligation proof
644+
for i in range(3):
645+
proof_file.delete_step(proof_file.steps_taken - 1)
646+
# Delete new Program definition and new Program lemma and proof
647+
proof_file.change_steps([CoqDelete(9) for _ in range(5)])
648+
649+
# Check the proofs
650+
assert len(proof_file.proofs) == 3
651+
assert len(proof_file.open_proofs) == 0
652+
for i, proof in enumerate(proof_file.proofs):
653+
assert proof.text == texts[i]
654+
assert proof.program is not None
655+
assert (
656+
proof.program.text
657+
== programs[i][0]
658+
+ " Definition "
659+
+ programs[i][1]
660+
+ " (n : nat) : { x : nat | x = n } := if dec (Nat.leb n 0) then 0%nat else "
661+
+ programs[i][2]
662+
+ "."
663+
)
664+
assert len(proof.steps) == 2
665+
assert proof.steps[0].text == "\n dummy_tactic n e."
666+
667+
668+
class TestProofChangeObligationWithType(SetupProofFile):
669+
def setup_method(self, method):
670+
self.setup("test_obligation_with_type.v")
671+
672+
@pytest.mark.skipif(
673+
version.parse(SetupProofFile.COQ_VERSION) >= version.parse("9.0"),
674+
reason='Obligations with " : type" are not included in Rocq 9.0+',
675+
)
561676
def test_change_obligation(self):
562677
proof_file = self.proof_file
563678

0 commit comments

Comments
 (0)