Skip to content

Commit 7a8e48c

Browse files
committed
proofnav: improve EasyCrypt error handling in Sphinx extension
Make EasyCrypt failures non-fatal during documentation builds. Errors are now reported as Sphinx warnings instead of stopping the build, with the last EasyCrypt error message printed in the log. Additionally, all reported errors are properly located, either in the corresponding .rst source or in the referenced EasyCrypt file.
1 parent 96b17a3 commit 7a8e48c

1 file changed

Lines changed: 103 additions & 30 deletions

File tree

doc/extensions/ecproofs/ecproofs.py

Lines changed: 103 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# --------------------------------------------------------------
22
from __future__ import annotations
3+
from typing import Any
34

45
import docutils as du
56

@@ -14,9 +15,13 @@
1415
import subprocess as subp
1516
import tempfile
1617

18+
1719
# ======================================================================
1820
ROOT = os.path.dirname(__file__)
1921

22+
# ======================================================================
23+
logger = su.logging.getLogger(__name__)
24+
2025
# ======================================================================
2126
class ProofnavNode(du.nodes.General, du.nodes.Element):
2227
@staticmethod
@@ -37,58 +42,95 @@ def depart_proofnav_node_html(self, node: ProofnavNode):
3742

3843
self.body.append(html)
3944

45+
# ======================================================================
46+
class EasyCryptError(se.SphinxError):
47+
category = "easycrypt"
48+
49+
# ======================================================================
50+
class EasyCrypt:
51+
@staticmethod
52+
def run(cmd, *, location: Any | None = None, warn_only: bool = True):
53+
try:
54+
proc = subp.run(
55+
cmd, check = True, text = True, capture_output = True,
56+
)
57+
logger.debug("Command stdout:\n%s", proc.stdout)
58+
logger.debug("Command stderr:\n%s", proc.stderr)
59+
60+
return True
61+
62+
except subp.CalledProcessError as e:
63+
msg = f"{cmd[0]} exited with code {e.returncode}"
64+
65+
if e.stdout:
66+
logger.debug("stdout:\n%s", e.stdout, location = location)
67+
if e.stderr:
68+
logger.debug("stderr:\n%s", e.stderr, location = location)
69+
70+
logs = [x.split(maxsplit = 1) for x in e.stderr.splitlines()]
71+
logs = [x[1] for x in logs if len(x) == 2 and x[0] == 'E']
72+
73+
for log in logs:
74+
logger.error(log, location = location, type = EasyCryptError.category)
75+
76+
logger.error(msg, location = location, type = EasyCryptError.category)
77+
78+
raise EasyCryptError(msg) from e
79+
4080
# ======================================================================
4181
class EasyCryptProofDirective(su.docutils.SphinxDirective):
82+
TRAP_RE = r'\(\*\s*\$\s*\*\)\s*'
83+
4284
has_content = True
4385

4486
option_spec = {
4587
'title': su.docutils.directives.unchanged,
4688
}
4789

48-
def run(self):
49-
env = self.state.document.settings.env
90+
def find_trap(self, source: str):
91+
location = (self.state.document.current_source, self.lineno)
5092

51-
rawcode = '\n'.join(self.content) + '\n'
93+
if (trap := re.search(self.TRAP_RE, source, re.MULTILINE)) is None:
94+
logger.error(
95+
'Cannot find the trap',
96+
location = location, type = EasyCryptError.category)
97+
raise EasyCryptError
5298

53-
# Find the trap
54-
if (trap := re.search(r'\(\*\s*\$\s*\*\)\s*', rawcode, re.MULTILINE)) is None:
55-
raise se.SphinxError('Cannot find the trap')
56-
code = rawcode[:trap.start()] + rawcode[trap.end():]
57-
58-
# Find the trap sentence number
59-
sentences = [
60-
m.end() - 1
61-
for m in re.finditer(r'\.(\s+|\$)', code)
62-
]
63-
sentence = bisect.bisect_left(sentences, trap.start())
99+
return trap
100+
101+
def run_easycrypt(self, source: str):
102+
location = (self.state.document.current_source, self.lineno)
64103

65-
# Run EasyCrypt and extract the proof trace
66104
with tempfile.TemporaryDirectory(delete = False) as tmpdir:
67105
ecfile = os.path.join(tmpdir, 'input.ec')
68106
ecofile = os.path.join(tmpdir, 'input.eco')
107+
69108
with open(ecfile, 'w') as ecstream:
70-
ecstream.write(code)
71-
subp.check_call(
72-
['easycrypt', 'compile', '-pragmas', 'Proofs:weak', '-trace', ecfile],
73-
stdout = subp.DEVNULL,
74-
stderr = subp.DEVNULL,
109+
ecstream.write(source)
110+
111+
EasyCrypt.run(
112+
['easycrypt', 'compile', '-script', '-pragmas', 'Proofs:weak', '-trace', ecfile],
113+
location = location
75114
)
115+
76116
with open(ecofile) as ecostream:
77-
eco = json.load(ecostream)
117+
return json.load(ecostream)
78118

79-
serial = env.new_serialno("proofnav")
119+
def create_widget(self, code: str, sentence: int, eco: Any):
120+
serial = self.state.document.settings.env.new_serialno("proofnav")
80121
uid = f"proofnav-{serial}"
81122

82-
# Create widget metadata
83-
data = dict()
84-
85-
data["source"] = code
86-
data["sentenceEnds"] = [x["position"] for x in eco["trace"][1:]]
87-
data["sentences"] = [
123+
sentences = [
88124
dict(goals = x["goals"], message = x["messages"])
89125
for x in eco["trace"][1:]
90126
]
91-
data["initialSentence"] = sentence - 1
127+
128+
data = dict(
129+
source = code,
130+
sentenceEnds = [x["position"] for x in eco["trace"][1:]],
131+
sentences = sentences,
132+
initialSentence = sentence - 1,
133+
)
92134

93135
if 'title' in self.options:
94136
data['title'] = self.options['title']
@@ -97,8 +139,39 @@ def run(self):
97139
node["uid"] = uid
98140
node["json"] = json.dumps(
99141
data, ensure_ascii = False, separators = (",", ":"), indent = 2)
142+
143+
return node
100144

101-
return [node]
145+
def run(self):
146+
try:
147+
rawcode = '\n'.join(self.content) + '\n'
148+
149+
# Find the trap and erase it
150+
trap = self.find_trap(rawcode)
151+
code = rawcode[:trap.start()] + rawcode[trap.end():]
152+
153+
# Find the trap sentence number
154+
sentences = [
155+
m.end() - 1
156+
for m in re.finditer(r'\.(\s+|\$)', code)
157+
]
158+
sentence = bisect.bisect_left(sentences, trap.start())
159+
160+
# Run EasyCrypt and extract the proof trace
161+
eco = self.run_easycrypt(code)
162+
163+
# Create the widget
164+
node = self.create_widget(code, sentence, eco)
165+
return [node]
166+
167+
except EasyCryptError:
168+
self.state.document.settings.env.note_reread()
169+
170+
fallback = du.nodes.literal_block(
171+
"[easycrypt failed]",
172+
"[easycrypt failed]",
173+
)
174+
return [fallback]
102175

103176
# ======================================================================
104177
def on_builder_inited(app: sa.Sphinx):

0 commit comments

Comments
 (0)