2 # SymbiYosys (sby) -- Front-end for Yosys-based formal verification flows
4 # Copyright (C) 2016 Clifford Wolf <clifford@clifford.at>
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.
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.
19 import os
, re
, sys
, signal
20 if os
.name
== "posix":
21 import resource
, fcntl
23 from shutil
import copyfile
, rmtree
24 from select
import select
25 from time
import time
, localtime
, sleep
27 all_tasks_running
= []
29 def force_shutdown(signum
, frame
):
30 print("SBY ---- Keyboard interrupt or external termination signal ----", flush
=True)
31 for task
in list(all_tasks_running
):
35 if os
.name
== "posix":
36 signal
.signal(signal
.SIGHUP
, force_shutdown
)
37 signal
.signal(signal
.SIGINT
, force_shutdown
)
38 signal
.signal(signal
.SIGTERM
, force_shutdown
)
40 def process_filename(filename
):
41 if filename
.startswith("~/"):
42 filename
= os
.environ
['HOME'] + filename
[1:]
44 filename
= os
.path
.expandvars(filename
)
49 def __init__(self
, job
, info
, deps
, cmdline
, logfile
=None, logstderr
=True, silent
=False):
52 self
.terminated
= False
53 self
.checkretcode
= False
57 if os
.name
== "posix":
58 self
.cmdline
= cmdline
60 # Windows command interpreter equivalents for sequential
61 # commands (; => &) command grouping ({} => ()).
68 cmdline_copy
= cmdline
69 for u
, w
in replacements
.items():
70 cmdline_copy
= cmdline_copy
.replace(u
, w
)
71 self
.cmdline
= cmdline_copy
72 self
.logfile
= logfile
73 self
.noprintregex
= None
76 self
.logstderr
= logstderr
79 self
.job
.tasks_pending
.append(self
)
82 dep
.register_dep(self
)
84 self
.output_callback
= None
85 self
.exit_callback
= None
87 def register_dep(self
, next_task
):
91 self
.notify
.append(next_task
)
94 if line
is not None and (self
.noprintregex
is None or not self
.noprintregex
.match(line
)):
95 if self
.logfile
is not None:
96 print(line
, file=self
.logfile
)
97 self
.job
.log("{}: {}".format(self
.info
, line
))
99 def handle_output(self
, line
):
100 if self
.terminated
or len(line
) == 0:
102 if self
.output_callback
is not None:
103 line
= self
.output_callback(line
)
106 def handle_exit(self
, retcode
):
109 if self
.logfile
is not None:
111 if self
.exit_callback
is not None:
112 self
.exit_callback(retcode
)
114 def terminate(self
, timeout
=False):
115 if self
.job
.opt_wait
and not timeout
:
119 self
.job
.log("{}: terminating process".format(self
.info
))
120 if os
.name
== "posix":
122 os
.killpg(self
.p
.pid
, signal
.SIGTERM
)
123 except PermissionError
:
126 self
.job
.tasks_running
.remove(self
)
127 all_tasks_running
.remove(self
)
128 self
.terminated
= True
131 if self
.finished
or self
.terminated
:
135 for dep
in self
.deps
:
140 self
.job
.log("{}: starting process \"{}\"".format(self
.info
, self
.cmdline
))
142 if os
.name
== "posix":
144 signal
.signal(signal
.SIGINT
, signal
.SIG_IGN
)
147 self
.p
= subprocess
.Popen(["/usr/bin/env", "bash", "-c", self
.cmdline
], stdin
=subprocess
.DEVNULL
, stdout
=subprocess
.PIPE
,
148 stderr
=(subprocess
.STDOUT
if self
.logstderr
else None), preexec_fn
=preexec_fn
)
150 fl
= fcntl
.fcntl(self
.p
.stdout
, fcntl
.F_GETFL
)
151 fcntl
.fcntl(self
.p
.stdout
, fcntl
.F_SETFL
, fl | os
.O_NONBLOCK
)
154 self
.p
= subprocess
.Popen(self
.cmdline
, shell
=True, stdin
=subprocess
.DEVNULL
, stdout
=subprocess
.PIPE
,
155 stderr
=(subprocess
.STDOUT
if self
.logstderr
else None))
157 self
.job
.tasks_pending
.remove(self
)
158 self
.job
.tasks_running
.append(self
)
159 all_tasks_running
.append(self
)
164 outs
= self
.p
.stdout
.readline().decode("utf-8")
165 if len(outs
) == 0: break
167 self
.linebuffer
+= outs
169 outs
= (self
.linebuffer
+ outs
).strip()
171 self
.handle_output(outs
)
173 if self
.p
.poll() is not None:
175 self
.job
.log("{}: finished (returncode={})".format(self
.info
, self
.p
.returncode
))
176 self
.job
.tasks_running
.remove(self
)
177 all_tasks_running
.remove(self
)
180 if self
.p
.returncode
== 127:
181 self
.job
.status
= "ERROR"
183 self
.job
.log("{}: COMMAND NOT FOUND. ERROR.".format(self
.info
))
184 self
.terminated
= True
188 self
.handle_exit(self
.p
.returncode
)
190 if self
.checkretcode
and self
.p
.returncode
!= 0:
191 self
.job
.status
= "ERROR"
193 self
.job
.log("{}: job failed. ERROR.".format(self
.info
))
194 self
.terminated
= True
199 for next_task
in self
.notify
:
204 class SbyAbort(BaseException
):
209 def __init__(self
, sbyconfig
, workdir
, early_logs
, reusedir
):
210 self
.options
= dict()
211 self
.used_options
= set()
212 self
.engines
= list()
215 self
.verbatim_files
= dict()
217 self
.workdir
= workdir
218 self
.reusedir
= reusedir
219 self
.status
= "UNKNOWN"
226 "smtbmc": "yosys-smtbmc",
227 "suprove": "suprove",
234 self
.tasks_running
= []
235 self
.tasks_pending
= []
237 self
.start_clock_time
= time()
239 if os
.name
== "posix":
240 ru
= resource
.getrusage(resource
.RUSAGE_CHILDREN
)
241 self
.start_process_time
= ru
.ru_utime
+ ru
.ru_stime
243 self
.summary
= list()
245 self
.logfile
= open("{}/logfile.txt".format(workdir
), "a")
247 for line
in early_logs
:
248 print(line
, file=self
.logfile
, flush
=True)
251 with
open("{}/config.sby".format(workdir
), "w") as f
:
252 for line
in sbyconfig
:
256 for task
in self
.tasks_pending
:
259 while len(self
.tasks_running
):
261 for task
in self
.tasks_running
:
263 fds
.append(task
.p
.stdout
)
265 if os
.name
== "posix":
267 select(fds
, [], [], 1.0) == ([], [], [])
268 except InterruptedError
:
273 for task
in self
.tasks_running
:
276 for task
in self
.tasks_pending
:
279 if self
.opt_timeout
is not None:
280 total_clock_time
= int(time() - self
.start_clock_time
)
281 if total_clock_time
> self
.opt_timeout
:
282 self
.log("Reached TIMEOUT ({} seconds). Terminating all tasks.".format(self
.opt_timeout
))
283 self
.status
= "TIMEOUT"
284 self
.terminate(timeout
=True)
286 def log(self
, logmessage
):
288 print("SBY {:2d}:{:02d}:{:02d} [{}] {}".format(tm
.tm_hour
, tm
.tm_min
, tm
.tm_sec
, self
.workdir
, logmessage
), flush
=True)
289 print("SBY {:2d}:{:02d}:{:02d} [{}] {}".format(tm
.tm_hour
, tm
.tm_min
, tm
.tm_sec
, self
.workdir
, logmessage
), file=self
.logfile
, flush
=True)
291 def error(self
, logmessage
):
293 print("SBY {:2d}:{:02d}:{:02d} [{}] ERROR: {}".format(tm
.tm_hour
, tm
.tm_min
, tm
.tm_sec
, self
.workdir
, logmessage
), flush
=True)
294 print("SBY {:2d}:{:02d}:{:02d} [{}] ERROR: {}".format(tm
.tm_hour
, tm
.tm_min
, tm
.tm_sec
, self
.workdir
, logmessage
), file=self
.logfile
, flush
=True)
295 self
.status
= "ERROR"
296 if "ERROR" not in self
.expect
:
299 with
open("{}/{}".format(self
.workdir
, self
.status
), "w") as f
:
300 print("ERROR: {}".format(logmessage
), file=f
)
301 raise SbyAbort(logmessage
)
303 def makedirs(self
, path
):
304 if self
.reusedir
and os
.path
.isdir(path
):
305 rmtree(path
, ignore_errors
=True)
309 os
.makedirs(self
.workdir
+ "/src")
311 for dstfile
, lines
in self
.verbatim_files
.items():
312 dstfile
= self
.workdir
+ "/src/" + dstfile
313 self
.log("Writing '{}'.".format(dstfile
))
315 with
open(dstfile
, "w") as f
:
319 for dstfile
, srcfile
in self
.files
.items():
320 if dstfile
.startswith("/") or dstfile
.startswith("../") or ("/../" in dstfile
):
321 self
.error("destination filename must be a relative path without /../: {}".format(dstfile
))
322 dstfile
= self
.workdir
+ "/src/" + dstfile
324 srcfile
= process_filename(srcfile
)
326 basedir
= os
.path
.dirname(dstfile
)
327 if basedir
!= "" and not os
.path
.exists(basedir
):
330 self
.log("Copy '{}' to '{}'.".format(srcfile
, dstfile
))
331 copyfile(srcfile
, dstfile
)
333 def handle_str_option(self
, option_name
, default_value
):
334 if option_name
in self
.options
:
335 self
.__dict
__["opt_" + option_name
] = self
.options
[option_name
]
336 self
.used_options
.add(option_name
)
338 self
.__dict
__["opt_" + option_name
] = default_value
340 def handle_int_option(self
, option_name
, default_value
):
341 if option_name
in self
.options
:
342 self
.__dict
__["opt_" + option_name
] = int(self
.options
[option_name
])
343 self
.used_options
.add(option_name
)
345 self
.__dict
__["opt_" + option_name
] = default_value
347 def handle_bool_option(self
, option_name
, default_value
):
348 if option_name
in self
.options
:
349 if self
.options
[option_name
] not in ["on", "off"]:
350 self
.error("Invalid value '{}' for boolean option {}.".format(self
.options
[option_name
], option_name
))
351 self
.__dict
__["opt_" + option_name
] = self
.options
[option_name
] == "on"
352 self
.used_options
.add(option_name
)
354 self
.__dict
__["opt_" + option_name
] = default_value
356 def make_model(self
, model_name
):
357 if not os
.path
.isdir("{}/model".format(self
.workdir
)):
358 os
.makedirs("{}/model".format(self
.workdir
))
360 if model_name
in ["base", "nomem"]:
361 with
open("{}/model/design{}.ys".format(self
.workdir
, "" if model_name
== "base" else "_nomem"), "w") as f
:
362 print("# running in {}/src/".format(self
.workdir
), file=f
)
363 for cmd
in self
.script
:
365 if model_name
== "base":
366 print("memory_nordff", file=f
)
368 print("memory_map", file=f
)
369 if self
.opt_multiclock
:
370 print("clk2fflogic", file=f
)
372 print("async2sync", file=f
)
373 print("chformal -assume -early", file=f
)
374 if self
.opt_mode
in ["bmc", "prove"]:
375 print("chformal -live -fair -cover -remove", file=f
)
376 if self
.opt_mode
== "cover":
377 print("chformal -live -fair -remove", file=f
)
378 if self
.opt_mode
== "live":
379 print("chformal -assert2assume", file=f
)
380 print("chformal -cover -remove", file=f
)
381 print("opt_clean", file=f
)
382 print("setundef -anyseq", file=f
)
383 print("opt -keepdc -fast", file=f
)
384 print("check", file=f
)
385 print("hierarchy -simcheck", file=f
)
386 print("write_ilang ../model/design{}.il".format("" if model_name
== "base" else "_nomem"), file=f
)
388 task
= SbyTask(self
, model_name
, [],
389 "cd {}/src; {} -ql ../model/design{s}.log ../model/design{s}.ys".format(self
.workdir
, self
.exe_paths
["yosys"],
390 s
="" if model_name
== "base" else "_nomem"))
391 task
.checkretcode
= True
395 if re
.match(r
"^smt2(_syn)?(_nomem)?(_stbv|_stdt)?$", model_name
):
396 with
open("{}/model/design_{}.ys".format(self
.workdir
, model_name
), "w") as f
:
397 print("# running in {}/model/".format(self
.workdir
), file=f
)
398 print("read_ilang design{}.il".format("_nomem" if "_nomem" in model_name
else ""), file=f
)
399 if "_syn" in model_name
:
400 print("techmap", file=f
)
401 print("opt -fast", file=f
)
403 print("opt_clean", file=f
)
404 print("stat", file=f
)
405 if "_stbv" in model_name
:
406 print("write_smt2 -stbv -wires design_{}.smt2".format(model_name
), file=f
)
407 elif "_stdt" in model_name
:
408 print("write_smt2 -stdt -wires design_{}.smt2".format(model_name
), file=f
)
410 print("write_smt2 -wires design_{}.smt2".format(model_name
), file=f
)
412 task
= SbyTask(self
, model_name
, self
.model("nomem" if "_nomem" in model_name
else "base"),
413 "cd {}/model; {} -ql design_{s}.log design_{s}.ys".format(self
.workdir
, self
.exe_paths
["yosys"], s
=model_name
))
414 task
.checkretcode
= True
418 if re
.match(r
"^btor(_syn)?(_nomem)?$", model_name
):
419 with
open("{}/model/design_{}.ys".format(self
.workdir
, model_name
), "w") as f
:
420 print("# running in {}/model/".format(self
.workdir
), file=f
)
421 print("read_ilang design{}.il".format("_nomem" if "_nomem" in model_name
else ""), file=f
)
422 print("flatten", file=f
)
423 print("setundef -undriven -anyseq", file=f
)
424 if "_syn" in model_name
:
425 print("opt -full", file=f
)
426 print("techmap", file=f
)
427 print("opt -fast", file=f
)
429 print("opt_clean", file=f
)
431 print("opt -fast", file=f
)
432 print("delete -output", file=f
)
433 print("stat", file=f
)
434 print("write_btor {}-i design_{m}.info design_{m}.btor".format("-c " if self
.opt_mode
== "cover" else "", m
=model_name
), file=f
)
436 task
= SbyTask(self
, model_name
, self
.model("nomem" if "_nomem" in model_name
else "base"),
437 "cd {}/model; {} -ql design_{s}.log design_{s}.ys".format(self
.workdir
, self
.exe_paths
["yosys"], s
=model_name
))
438 task
.checkretcode
= True
442 if model_name
== "aig":
443 with
open("{}/model/design_aiger.ys".format(self
.workdir
), "w") as f
:
444 print("# running in {}/model/".format(self
.workdir
), file=f
)
445 print("read_ilang design_nomem.il", file=f
)
446 print("flatten", file=f
)
447 print("setundef -undriven -anyseq", file=f
)
448 print("setattr -unset keep", file=f
)
449 print("delete -output", file=f
)
450 print("opt -full", file=f
)
451 print("techmap", file=f
)
452 print("opt -fast", file=f
)
453 print("abc -g AND -fast", file=f
)
454 print("opt_clean", file=f
)
455 print("stat", file=f
)
456 print("write_aiger -I -B -zinit -map design_aiger.aim design_aiger.aig", file=f
)
458 task
= SbyTask(self
, "aig", self
.model("nomem"),
459 "cd {}/model; {} -ql design_aiger.log design_aiger.ys".format(self
.workdir
, self
.exe_paths
["yosys"]))
460 task
.checkretcode
= True
466 def model(self
, model_name
):
467 if model_name
not in self
.models
:
468 self
.models
[model_name
] = self
.make_model(model_name
)
469 return self
.models
[model_name
]
471 def terminate(self
, timeout
=False):
472 for task
in list(self
.tasks_running
):
473 task
.terminate(timeout
=timeout
)
475 def update_status(self
, new_status
):
476 assert new_status
in ["PASS", "FAIL", "UNKNOWN", "ERROR"]
478 if new_status
== "UNKNOWN":
481 if self
.status
== "ERROR":
484 if new_status
== "PASS":
485 assert self
.status
!= "FAIL"
488 elif new_status
== "FAIL":
489 assert self
.status
!= "PASS"
492 elif new_status
== "ERROR":
493 self
.status
= "ERROR"
498 def run(self
, setupmode
):
502 with
open("{}/config.sby".format(self
.workdir
), "r") as f
:
505 if mode
in ["options", "engines", "files"]:
506 line
= re
.sub(r
"\s*(\s#.*)?$", "", line
)
507 if line
== "" or line
[0] == "#":
512 if mode
is None and (len(line
) == 0 or line
[0] == "#"):
514 match
= re
.match(r
"^\s*\[(.*)\]\s*$", line
)
516 entries
= match
.group(1).split()
517 if len(entries
) == 0:
518 self
.error("sby file syntax error: {}".format(line
))
520 if entries
[0] == "options":
522 if len(self
.options
) != 0 or len(entries
) != 1:
523 self
.error("sby file syntax error: {}".format(line
))
526 if entries
[0] == "engines":
528 if len(self
.engines
) != 0 or len(entries
) != 1:
529 self
.error("sby file syntax error: {}".format(line
))
532 if entries
[0] == "script":
534 if len(self
.script
) != 0 or len(entries
) != 1:
535 self
.error("sby file syntax error: {}".format(line
))
538 if entries
[0] == "file":
540 if len(entries
) != 2:
541 self
.error("sby file syntax error: {}".format(line
))
542 current_verbatim_file
= entries
[1]
543 if current_verbatim_file
in self
.verbatim_files
:
544 self
.error("duplicate file: {}".format(entries
[1]))
545 self
.verbatim_files
[current_verbatim_file
] = list()
548 if entries
[0] == "files":
550 if len(entries
) != 1:
551 self
.error("sby file syntax error: {}".format(line
))
554 self
.error("sby file syntax error: {}".format(line
))
556 if mode
== "options":
557 entries
= line
.split()
558 if len(entries
) != 2:
559 self
.error("sby file syntax error: {}".format(line
))
560 self
.options
[entries
[0]] = entries
[1]
563 if mode
== "engines":
564 entries
= line
.split()
565 self
.engines
.append(entries
)
569 self
.script
.append(line
)
573 entries
= line
.split()
574 if len(entries
) == 1:
575 self
.files
[os
.path
.basename(entries
[0])] = entries
[0]
576 elif len(entries
) == 2:
577 self
.files
[entries
[0]] = entries
[1]
579 self
.error("sby file syntax error: {}".format(line
))
583 self
.verbatim_files
[current_verbatim_file
].append(raw_line
)
586 self
.error("sby file syntax error: {}".format(line
))
588 self
.handle_str_option("mode", None)
590 if self
.opt_mode
not in ["bmc", "prove", "cover", "live"]:
591 self
.error("Invalid mode: {}".format(self
.opt_mode
))
593 self
.expect
= ["PASS"]
594 if "expect" in self
.options
:
595 self
.expect
= self
.options
["expect"].upper().split(",")
596 self
.used_options
.add("expect")
598 for s
in self
.expect
:
599 if s
not in ["PASS", "FAIL", "UNKNOWN", "ERROR", "TIMEOUT"]:
600 self
.error("Invalid expect value: {}".format(s
))
602 self
.handle_bool_option("multiclock", False)
603 self
.handle_bool_option("wait", False)
604 self
.handle_int_option("timeout", None)
606 self
.handle_str_option("smtc", None)
607 self
.handle_int_option("skip", None)
608 self
.handle_str_option("tbtop", None)
610 if self
.opt_smtc
is not None:
611 for engine
in self
.engines
:
612 if engine
[0] != "smtbmc":
613 self
.error("Option smtc is only valid for smtbmc engine.")
615 if self
.opt_skip
is not None:
616 if self
.opt_skip
== 0:
619 for engine
in self
.engines
:
620 if engine
[0] not in ["smtbmc", "btor"]:
621 self
.error("Option skip is only valid for smtbmc and btor engines.")
623 if len(self
.engines
) == 0:
624 self
.error("Config file is lacking engine configuration.")
627 rmtree("{}/model".format(self
.workdir
), ignore_errors
=True)
635 if self
.opt_mode
== "bmc":
637 sby_mode_bmc
.run(self
)
639 elif self
.opt_mode
== "prove":
640 import sby_mode_prove
641 sby_mode_prove
.run(self
)
643 elif self
.opt_mode
== "live":
645 sby_mode_live
.run(self
)
647 elif self
.opt_mode
== "cover":
648 import sby_mode_cover
649 sby_mode_cover
.run(self
)
654 for opt
in self
.options
.keys():
655 if opt
not in self
.used_options
:
656 self
.error("Unused option: {}".format(opt
))
660 total_clock_time
= int(time() - self
.start_clock_time
)
662 if os
.name
== "posix":
663 ru
= resource
.getrusage(resource
.RUSAGE_CHILDREN
)
664 total_process_time
= int((ru
.ru_utime
+ ru
.ru_stime
) - self
.start_process_time
)
665 self
.total_time
= total_process_time
668 "Elapsed clock time [H:MM:SS (secs)]: {}:{:02d}:{:02d} ({})".format
669 (total_clock_time
// (60*60), (total_clock_time
// 60) % 60, total_clock_time
% 60, total_clock_time
),
670 "Elapsed process time [H:MM:SS (secs)]: {}:{:02d}:{:02d} ({})".format
671 (total_process_time
// (60*60), (total_process_time
// 60) % 60, total_process_time
% 60, total_process_time
),
675 "Elapsed clock time [H:MM:SS (secs)]: {}:{:02d}:{:02d} ({})".format
676 (total_clock_time
// (60*60), (total_clock_time
// 60) % 60, total_clock_time
% 60, total_clock_time
),
677 "Elapsed process time unvailable on Windows"
680 for line
in self
.summary
:
681 self
.log("summary: {}".format(line
))
683 assert self
.status
in ["PASS", "FAIL", "UNKNOWN", "ERROR", "TIMEOUT"]
685 if self
.status
in self
.expect
:
688 if self
.status
== "PASS": self
.retcode
= 1
689 if self
.status
== "FAIL": self
.retcode
= 2
690 if self
.status
== "UNKNOWN": self
.retcode
= 4
691 if self
.status
== "TIMEOUT": self
.retcode
= 8
692 if self
.status
== "ERROR": self
.retcode
= 16
694 with
open("{}/{}".format(self
.workdir
, self
.status
), "w") as f
:
695 for line
in self
.summary
: