junit: handle multiple asserts failing with the same trace
[SymbiYosys.git] / sbysrc / sby_engine_smtbmc.py
1 #
2 # SymbiYosys (sby) -- Front-end for Yosys-based formal verification flows
3 #
4 # Copyright (C) 2016 Claire Xenia Wolf <claire@yosyshq.com>
5 #
6 # Permission to use, copy, modify, and/or distribute this software for any
7 # purpose with or without fee is hereby granted, provided that the above
8 # copyright notice and this permission notice appear in all copies.
9 #
10 # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11 # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13 # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17 #
18
19 import re, os, getopt
20 from sby_core import SbyProc
21
22 def run(mode, task, engine_idx, engine):
23 smtbmc_opts = []
24 nomem_opt = False
25 presat_opt = True
26 unroll_opt = None
27 syn_opt = False
28 stbv_opt = False
29 stdt_opt = False
30 dumpsmt2 = False
31 progress = False
32 basecase_only = False
33 induction_only = False
34 random_seed = None
35 task.precise_prop_status = True
36
37 opts, args = getopt.getopt(engine[1:], "", ["nomem", "syn", "stbv", "stdt", "presat",
38 "nopresat", "unroll", "nounroll", "dumpsmt2", "progress", "basecase", "induction", "seed="])
39
40 for o, a in opts:
41 if o == "--nomem":
42 nomem_opt = True
43 elif o == "--syn":
44 syn_opt = True
45 elif o == "--stbv":
46 stbv_opt = True
47 elif o == "--stdt":
48 stdt_opt = True
49 elif o == "--presat":
50 presat_opt = True
51 elif o == "--nopresat":
52 presat_opt = False
53 elif o == "--unroll":
54 unroll_opt = True
55 elif o == "--nounroll":
56 unroll_opt = False
57 elif o == "--dumpsmt2":
58 dumpsmt2 = True
59 elif o == "--progress":
60 progress = True
61 elif o == "--basecase":
62 if induction_only:
63 task.error("smtbmc options --basecase and --induction are exclusive.")
64 basecase_only = True
65 elif o == "--induction":
66 if basecase_only:
67 task.error("smtbmc options --basecase and --induction are exclusive.")
68 induction_only = True
69 elif o == "--seed":
70 random_seed = a
71 else:
72 task.error(f"Invalid smtbmc options {o}.")
73
74 xtra_opts = False
75 for i, a in enumerate(args):
76 if i == 0 and a == "z3" and unroll_opt is None:
77 unroll_opt = False
78 if a == "--":
79 xtra_opts = True
80 continue
81 if xtra_opts:
82 smtbmc_opts.append(a)
83 else:
84 smtbmc_opts += ["-s" if i == 0 else "-S", a]
85
86 if presat_opt:
87 smtbmc_opts += ["--presat"]
88
89 if unroll_opt is None or unroll_opt:
90 smtbmc_opts += ["--unroll"]
91
92 if task.opt_smtc is not None:
93 smtbmc_opts += ["--smtc", f"src/{task.opt_smtc}"]
94
95 if task.opt_tbtop is not None:
96 smtbmc_opts += ["--vlogtb-top", task.opt_tbtop]
97
98 model_name = "smt2"
99 if syn_opt: model_name += "_syn"
100 if nomem_opt: model_name += "_nomem"
101 if stbv_opt: model_name += "_stbv"
102 if stdt_opt: model_name += "_stdt"
103
104 if mode == "prove":
105 if not induction_only:
106 run("prove_basecase", task, engine_idx, engine)
107 if not basecase_only:
108 run("prove_induction", task, engine_idx, engine)
109 return
110
111 procname = f"engine_{engine_idx}"
112 trace_prefix = f"engine_{engine_idx}/trace"
113 logfile_prefix = f"{task.workdir}/engine_{engine_idx}/logfile"
114
115 if mode == "prove_basecase":
116 procname += ".basecase"
117 logfile_prefix += "_basecase"
118
119 if mode == "prove_induction":
120 procname += ".induction"
121 trace_prefix += "_induct"
122 logfile_prefix += "_induction"
123 smtbmc_opts.append("-i")
124
125 if mode == "cover":
126 smtbmc_opts.append("-c")
127 trace_prefix += "%"
128
129 if dumpsmt2:
130 smtbmc_opts += ["--dump-smt2", trace_prefix.replace("%", "") + ".smt2"]
131
132 if not progress:
133 smtbmc_opts.append("--noprogress")
134
135
136 if task.opt_skip is not None:
137 t_opt = "{}:{}".format(task.opt_skip, task.opt_depth)
138 else:
139 t_opt = "{}".format(task.opt_depth)
140
141 random_seed = f"--info \"(set-option :random-seed {random_seed})\"" if random_seed else ""
142 proc = SbyProc(
143 task,
144 procname,
145 task.model(model_name),
146 f"""cd {task.workdir}; {task.exe_paths["smtbmc"]} {" ".join(smtbmc_opts)} -t {t_opt} {random_seed} --append {task.opt_append} --dump-vcd {trace_prefix}.vcd --dump-vlogtb {trace_prefix}_tb.v --dump-smtc {trace_prefix}.smtc model/design_{model_name}.smt2""",
147 logfile=open(logfile_prefix + ".txt", "w"),
148 logstderr=(not progress)
149 )
150
151 if mode == "prove_basecase":
152 task.basecase_procs.append(proc)
153
154 if mode == "prove_induction":
155 task.induction_procs.append(proc)
156
157 proc_status = None
158 last_prop = []
159
160 def output_callback(line):
161 nonlocal proc_status
162 nonlocal last_prop
163
164 match = re.match(r"^## [0-9: ]+ Status: FAILED", line)
165 if match:
166 proc_status = "FAIL"
167 return line.replace("FAILED", "failed")
168
169 match = re.match(r"^## [0-9: ]+ Status: PASSED", line)
170 if match:
171 proc_status = "PASS"
172 return line.replace("PASSED", "passed")
173
174 match = re.match(r"^## [0-9: ]+ Status: PREUNSAT", line)
175 if match:
176 proc_status = "ERROR"
177 return line
178
179 match = re.match(r"^## [0-9: ]+ Unexpected response from solver:", line)
180 if match:
181 proc_status = "ERROR"
182 return line
183
184 match = re.match(r"^## [0-9: ]+ Assert failed in (\S+): (\S+) \((\S+)\)", line)
185 if match:
186 cell_name = match[3]
187 prop = task.design_hierarchy.find_property_by_cellname(cell_name)
188 prop.status = "FAIL"
189 last_prop.append(prop)
190 return line
191
192 match = re.match(r"^## [0-9: ]+ Reached cover statement at (\S+) \((\S+)\) in step \d+.", line)
193 if match:
194 cell_name = match[2]
195 prop = task.design_hierarchy.find_property_by_cellname(cell_name)
196 prop.status = "PASS"
197 last_prop.append(prop)
198 return line
199
200 match = re.match(r"^## [0-9: ]+ Writing trace to VCD file: (\S+)", line)
201 if match and last_prop:
202 for p in last_prop:
203 p.tracefile = match[1]
204 last_prop = []
205 return line
206
207 match = re.match(r"^## [0-9: ]+ Unreached cover statement at (\S+) \((\S+)\).", line)
208 if match:
209 cell_name = match[2]
210 prop = task.design_hierarchy.find_property_by_cellname(cell_name)
211 prop.status = "FAIL"
212
213 return line
214
215 def exit_callback(retcode):
216 if proc_status is None:
217 task.error(f"engine_{engine_idx}: Engine terminated without status.")
218
219 if mode == "bmc" or mode == "cover":
220 task.update_status(proc_status)
221 proc_status_lower = proc_status.lower() if proc_status == "PASS" else proc_status
222 task.log(f"engine_{engine_idx}: Status returned by engine: {proc_status_lower}")
223 task.summary.append(f"""engine_{engine_idx} ({" ".join(engine)}) returned {proc_status_lower}""")
224
225 if proc_status == "FAIL" and mode != "cover":
226 if os.path.exists(f"{task.workdir}/engine_{engine_idx}/trace.vcd"):
227 task.summary.append(f"counterexample trace: {task.workdir}/engine_{engine_idx}/trace.vcd")
228 elif proc_status == "PASS" and mode == "cover":
229 print_traces_max = 5
230 for i in range(print_traces_max):
231 if os.path.exists(f"{task.workdir}/engine_{engine_idx}/trace{i}.vcd"):
232 task.summary.append(f"trace: {task.workdir}/engine_{engine_idx}/trace{i}.vcd")
233 else:
234 break
235 else:
236 excess_traces = 0
237 while os.path.exists(f"{task.workdir}/engine_{engine_idx}/trace{print_traces_max + excess_traces}.vcd"):
238 excess_traces += 1
239 if excess_traces > 0:
240 task.summary.append(f"""and {excess_traces} further trace{"s" if excess_traces > 1 else ""}""")
241 elif proc_status == "PASS" and mode == "bmc":
242 for prop in task.design_hierarchy:
243 if prop.type == prop.Type.ASSERT and prop.status == "UNKNOWN":
244 prop.status = "PASS"
245
246 task.terminate()
247
248 elif mode in ["prove_basecase", "prove_induction"]:
249 proc_status_lower = proc_status.lower() if proc_status == "PASS" else proc_status
250 task.log(f"""engine_{engine_idx}: Status returned by engine for {mode.split("_")[1]}: {proc_status_lower}""")
251 task.summary.append(f"""engine_{engine_idx} ({" ".join(engine)}) returned {proc_status_lower} for {mode.split("_")[1]}""")
252
253 if mode == "prove_basecase":
254 for proc in task.basecase_procs:
255 proc.terminate()
256
257 if proc_status == "PASS":
258 task.basecase_pass = True
259
260 else:
261 task.update_status(proc_status)
262 if os.path.exists(f"{task.workdir}/engine_{engine_idx}/trace.vcd"):
263 task.summary.append(f"counterexample trace: {task.workdir}/engine_{engine_idx}/trace.vcd")
264 task.terminate()
265
266 elif mode == "prove_induction":
267 for proc in task.induction_procs:
268 proc.terminate()
269
270 if proc_status == "PASS":
271 task.induction_pass = True
272
273 else:
274 assert False
275
276 if task.basecase_pass and task.induction_pass:
277 for prop in task.design_hierarchy:
278 if prop.type == prop.Type.ASSERT and prop.status == "UNKNOWN":
279 prop.status = "PASS"
280 task.update_status("PASS")
281 task.summary.append("successful proof by k-induction.")
282 task.terminate()
283
284 else:
285 assert False
286
287 proc.output_callback = output_callback
288 proc.exit_callback = exit_callback