11# --------------------------------------------------------------
22from __future__ import annotations
3+ from typing import Any
34
45import docutils as du
56
1415import subprocess as subp
1516import tempfile
1617
18+
1719# ======================================================================
1820ROOT = os .path .dirname (__file__ )
1921
22+ # ======================================================================
23+ logger = su .logging .getLogger (__name__ )
24+
2025# ======================================================================
2126class 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# ======================================================================
4181class 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# ======================================================================
104177def on_builder_inited (app : sa .Sphinx ):
0 commit comments