dbc05d323c3bc7631ca82ad4f06fe97b82beeb92
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 ({} => ()).
67 parts
= cmdline
.split("'")
68 for i
in range(len(parts
)):
70 cmdline_copy
= parts
[i
]
71 for u
, w
in replacements
.items():
72 cmdline_copy
= cmdline_copy
.replace(u
, w
)
73 parts
[i
] = cmdline_copy
74 self
.cmdline
= '"'.join(parts
)
75 self
.logfile
= logfile
76 self
.noprintregex
= None
79 self
.logstderr
= logstderr
82 self
.job
.tasks_pending
.append(self
)
85 dep
.register_dep(self
)
87 self
.output_callback
= None
88 self
.exit_callback
= None
90 def register_dep(self
, next_task
):
94 self
.notify
.append(next_task
)
97 if line
is not None and (self
.noprintregex
is None or not self
.noprintregex
.match(line
)):
98 if self
.logfile
is not None:
99 print(line
, file=self
.logfile
)
100 self
.job
.log(f
"{self.info}: {line}")
102 def handle_output(self
, line
):
103 if self
.terminated
or len(line
) == 0:
105 if self
.output_callback
is not None:
106 line
= self
.output_callback(line
)
109 def handle_exit(self
, retcode
):
112 if self
.logfile
is not None:
114 if self
.exit_callback
is not None:
115 self
.exit_callback(retcode
)
117 def terminate(self
, timeout
=False):
118 if self
.job
.opt_wait
and not timeout
:
122 self
.job
.log(f
"{self.info}: terminating process")
123 if os
.name
== "posix":
125 os
.killpg(self
.p
.pid
, signal
.SIGTERM
)
126 except PermissionError
:
129 self
.job
.tasks_running
.remove(self
)
130 all_tasks_running
.remove(self
)
131 self
.terminated
= True
134 if self
.finished
or self
.terminated
:
138 for dep
in self
.deps
:
143 self
.job
.log(f
"{self.info}: starting process \"{self.cmdline}\"")
145 if os
.name
== "posix":
147 signal
.signal(signal
.SIGINT
, signal
.SIG_IGN
)
150 self
.p
= subprocess
.Popen(["/usr/bin/env", "bash", "-c", self
.cmdline
], stdin
=subprocess
.DEVNULL
, stdout
=subprocess
.PIPE
,
151 stderr
=(subprocess
.STDOUT
if self
.logstderr
else None), preexec_fn
=preexec_fn
)
153 fl
= fcntl
.fcntl(self
.p
.stdout
, fcntl
.F_GETFL
)
154 fcntl
.fcntl(self
.p
.stdout
, fcntl
.F_SETFL
, fl | os
.O_NONBLOCK
)
157 self
.p
= subprocess
.Popen(self
.cmdline
, shell
=True, stdin
=subprocess
.DEVNULL
, stdout
=subprocess
.PIPE
,
158 stderr
=(subprocess
.STDOUT
if self
.logstderr
else None))
160 self
.job
.tasks_pending
.remove(self
)
161 self
.job
.tasks_running
.append(self
)
162 all_tasks_running
.append(self
)
167 outs
= self
.p
.stdout
.readline().decode("utf-8")
168 if len(outs
) == 0: break
170 self
.linebuffer
+= outs
172 outs
= (self
.linebuffer
+ outs
).strip()
174 self
.handle_output(outs
)
176 if self
.p
.poll() is not None:
178 self
.job
.log(f
"{self.info}: finished (returncode={self.p.returncode})")
179 self
.job
.tasks_running
.remove(self
)
180 all_tasks_running
.remove(self
)
183 if self
.p
.returncode
== 127:
184 self
.job
.status
= "ERROR"
186 self
.job
.log(f
"{self.info}: COMMAND NOT FOUND. ERROR.")
187 self
.terminated
= True
191 self
.handle_exit(self
.p
.returncode
)
193 if self
.checkretcode
and self
.p
.returncode
!= 0:
194 self
.job
.status
= "ERROR"
196 self
.job
.log(f
"{self.info}: job failed. ERROR.")
197 self
.terminated
= True
202 for next_task
in self
.notify
:
207 class SbyAbort(BaseException
):
212 def __init__(self
, sbyconfig
, workdir
, early_logs
, reusedir
):
213 self
.options
= dict()
214 self
.used_options
= set()
215 self
.engines
= list()
218 self
.verbatim_files
= dict()
220 self
.workdir
= workdir
221 self
.reusedir
= reusedir
222 self
.status
= "UNKNOWN"
226 yosys_program_prefix
= "" ##yosys-program-prefix##
228 "yosys": os
.getenv("YOSYS", yosys_program_prefix
+ "yosys"),
229 "abc": os
.getenv("ABC", yosys_program_prefix
+ "yosys-abc"),
230 "smtbmc": os
.getenv("SMTBMC", yosys_program_prefix
+ "yosys-smtbmc"),
231 "suprove": os
.getenv("SUPROVE", "suprove"),
232 "aigbmc": os
.getenv("AIGBMC", "aigbmc"),
233 "avy": os
.getenv("AVY", "avy"),
234 "btormc": os
.getenv("BTORMC", "btormc"),
235 "pono": os
.getenv("PONO", "pono"),
238 self
.tasks_running
= []
239 self
.tasks_pending
= []
241 self
.start_clock_time
= time()
243 if os
.name
== "posix":
244 ru
= resource
.getrusage(resource
.RUSAGE_CHILDREN
)
245 self
.start_process_time
= ru
.ru_utime
+ ru
.ru_stime
247 self
.summary
= list()
249 self
.logfile
= open(f
"{workdir}/logfile.txt", "a")
251 for line
in early_logs
:
252 print(line
, file=self
.logfile
, flush
=True)
255 with
open(f
"{workdir}/config.sby", "w") as f
:
256 for line
in sbyconfig
:
260 for task
in self
.tasks_pending
:
263 while len(self
.tasks_running
):
265 for task
in self
.tasks_running
:
267 fds
.append(task
.p
.stdout
)
269 if os
.name
== "posix":
271 select(fds
, [], [], 1.0) == ([], [], [])
272 except InterruptedError
:
277 for task
in self
.tasks_running
:
280 for task
in self
.tasks_pending
:
283 if self
.opt_timeout
is not None:
284 total_clock_time
= int(time() - self
.start_clock_time
)
285 if total_clock_time
> self
.opt_timeout
:
286 self
.log(f
"Reached TIMEOUT ({self.opt_timeout} seconds). Terminating all tasks.")
287 self
.status
= "TIMEOUT"
288 self
.terminate(timeout
=True)
290 def log(self
, logmessage
):
292 print("SBY {:2d}:{:02d}:{:02d} [{}] {}".format(tm
.tm_hour
, tm
.tm_min
, tm
.tm_sec
, self
.workdir
, logmessage
), flush
=True)
293 print("SBY {:2d}:{:02d}:{:02d} [{}] {}".format(tm
.tm_hour
, tm
.tm_min
, tm
.tm_sec
, self
.workdir
, logmessage
), file=self
.logfile
, flush
=True)
295 def error(self
, logmessage
):
297 print("SBY {:2d}:{:02d}:{:02d} [{}] ERROR: {}".format(tm
.tm_hour
, tm
.tm_min
, tm
.tm_sec
, self
.workdir
, logmessage
), flush
=True)
298 print("SBY {:2d}:{:02d}:{:02d} [{}] ERROR: {}".format(tm
.tm_hour
, tm
.tm_min
, tm
.tm_sec
, self
.workdir
, logmessage
), file=self
.logfile
, flush
=True)
299 self
.status
= "ERROR"
300 if "ERROR" not in self
.expect
:
303 with
open(f
"{self.workdir}/{self.status}", "w") as f
:
304 print(f
"ERROR: {logmessage}", file=f
)
305 raise SbyAbort(logmessage
)
307 def makedirs(self
, path
):
308 if self
.reusedir
and os
.path
.isdir(path
):
309 rmtree(path
, ignore_errors
=True)
313 os
.makedirs(self
.workdir
+ "/src")
315 for dstfile
, lines
in self
.verbatim_files
.items():
316 dstfile
= self
.workdir
+ "/src/" + dstfile
317 self
.log(f
"Writing '{dstfile}'.")
319 with
open(dstfile
, "w") as f
:
323 for dstfile
, srcfile
in self
.files
.items():
324 if dstfile
.startswith("/") or dstfile
.startswith("../") or ("/../" in dstfile
):
325 self
.error(f
"destination filename must be a relative path without /../: {dstfile}")
326 dstfile
= self
.workdir
+ "/src/" + dstfile
328 srcfile
= process_filename(srcfile
)
330 basedir
= os
.path
.dirname(dstfile
)
331 if basedir
!= "" and not os
.path
.exists(basedir
):
334 self
.log(f
"Copy '{os.path.abspath(srcfile)}' to '{os.path.abspath(dstfile)}'.")
335 copyfile(srcfile
, dstfile
)
337 def handle_str_option(self
, option_name
, default_value
):
338 if option_name
in self
.options
:
339 self
.__dict
__["opt_" + option_name
] = self
.options
[option_name
]
340 self
.used_options
.add(option_name
)
342 self
.__dict
__["opt_" + option_name
] = default_value
344 def handle_int_option(self
, option_name
, default_value
):
345 if option_name
in self
.options
:
346 self
.__dict
__["opt_" + option_name
] = int(self
.options
[option_name
])
347 self
.used_options
.add(option_name
)
349 self
.__dict
__["opt_" + option_name
] = default_value
351 def handle_bool_option(self
, option_name
, default_value
):
352 if option_name
in self
.options
:
353 if self
.options
[option_name
] not in ["on", "off"]:
354 self
.error(f
"Invalid value '{self.options[option_name]}' for boolean option {option_name}.")
355 self
.__dict
__["opt_" + option_name
] = self
.options
[option_name
] == "on"
356 self
.used_options
.add(option_name
)
358 self
.__dict
__["opt_" + option_name
] = default_value
360 def make_model(self
, model_name
):
361 if not os
.path
.isdir(f
"{self.workdir}/model"):
362 os
.makedirs(f
"{self.workdir}/model")
364 if model_name
in ["base", "nomem"]:
365 with
open(f
"""{self.workdir}/model/design{"" if model_name == "base" else "_nomem"}.ys""", "w") as f
:
366 print(f
"# running in {self.workdir}/src/", file=f
)
367 for cmd
in self
.script
:
369 if model_name
== "base":
370 print("memory_nordff", file=f
)
372 print("memory_map", file=f
)
373 if self
.opt_multiclock
:
374 print("clk2fflogic", file=f
)
376 print("async2sync", file=f
)
377 print("chformal -assume -early", file=f
)
378 if self
.opt_mode
in ["bmc", "prove"]:
379 print("chformal -live -fair -cover -remove", file=f
)
380 if self
.opt_mode
== "cover":
381 print("chformal -live -fair -remove", file=f
)
382 if self
.opt_mode
== "live":
383 print("chformal -assert2assume", file=f
)
384 print("chformal -cover -remove", file=f
)
385 print("opt_clean", file=f
)
386 print("setundef -anyseq", file=f
)
387 print("opt -keepdc -fast", file=f
)
388 print("check", file=f
)
389 print("hierarchy -simcheck", file=f
)
390 print(f
"""write_ilang ../model/design{"" if model_name == "base" else "_nomem"}.il""", file=f
)
396 "cd {}/src; {} -ql ../model/design{s}.log ../model/design{s}.ys".format(self
.workdir
, self
.exe_paths
["yosys"],
397 s
="" if model_name
== "base" else "_nomem")
399 task
.checkretcode
= True
403 if re
.match(r
"^smt2(_syn)?(_nomem)?(_stbv|_stdt)?$", model_name
):
404 with
open(f
"{self.workdir}/model/design_{model_name}.ys", "w") as f
:
405 print(f
"# running in {self.workdir}/model/", file=f
)
406 print(f
"""read_ilang design{"_nomem" if "_nomem" in model_name else ""}.il""", file=f
)
407 if "_syn" in model_name
:
408 print("techmap", file=f
)
409 print("opt -fast", file=f
)
411 print("opt_clean", file=f
)
412 print("dffunmap", file=f
)
413 print("stat", file=f
)
414 if "_stbv" in model_name
:
415 print(f
"write_smt2 -stbv -wires design_{model_name}.smt2", file=f
)
416 elif "_stdt" in model_name
:
417 print(f
"write_smt2 -stdt -wires design_{model_name}.smt2", file=f
)
419 print(f
"write_smt2 -wires design_{model_name}.smt2", file=f
)
424 self
.model("nomem" if "_nomem" in model_name
else "base"),
425 "cd {}/model; {} -ql design_{s}.log design_{s}.ys".format(self
.workdir
, self
.exe_paths
["yosys"], s
=model_name
)
427 task
.checkretcode
= True
431 if re
.match(r
"^btor(_syn)?(_nomem)?$", model_name
):
432 with
open(f
"{self.workdir}/model/design_{model_name}.ys", "w") as f
:
433 print(f
"# running in {self.workdir}/model/", file=f
)
434 print(f
"""read_ilang design{"_nomem" if "_nomem" in model_name else ""}.il""", file=f
)
435 print("flatten", file=f
)
436 print("setundef -undriven -anyseq", file=f
)
437 if "_syn" in model_name
:
438 print("opt -full", file=f
)
439 print("techmap", file=f
)
440 print("opt -fast", file=f
)
442 print("opt_clean", file=f
)
444 print("opt -fast", file=f
)
445 print("delete -output", file=f
)
446 print("dffunmap", file=f
)
447 print("stat", file=f
)
448 print("write_btor {}-i design_{m}.info design_{m}.btor".format("-c " if self
.opt_mode
== "cover" else "", m
=model_name
), file=f
)
453 self
.model("nomem" if "_nomem" in model_name
else "base"),
454 "cd {}/model; {} -ql design_{s}.log design_{s}.ys".format(self
.workdir
, self
.exe_paths
["yosys"], s
=model_name
)
456 task
.checkretcode
= True
460 if model_name
== "aig":
461 with
open(f
"{self.workdir}/model/design_aiger.ys", "w") as f
:
462 print(f
"# running in {self.workdir}/model/", file=f
)
463 print("read_ilang design_nomem.il", file=f
)
464 print("flatten", file=f
)
465 print("setundef -undriven -anyseq", file=f
)
466 print("setattr -unset keep", file=f
)
467 print("delete -output", file=f
)
468 print("opt -full", file=f
)
469 print("techmap", file=f
)
470 print("opt -fast", file=f
)
471 print("dffunmap", file=f
)
472 print("abc -g AND -fast", file=f
)
473 print("opt_clean", file=f
)
474 print("stat", file=f
)
475 print("write_aiger -I -B -zinit -map design_aiger.aim design_aiger.aig", file=f
)
481 f
"""cd {self.workdir}/model; {self.exe_paths["yosys"]} -ql design_aiger.log design_aiger.ys"""
483 task
.checkretcode
= True
489 def model(self
, model_name
):
490 if model_name
not in self
.models
:
491 self
.models
[model_name
] = self
.make_model(model_name
)
492 return self
.models
[model_name
]
494 def terminate(self
, timeout
=False):
495 for task
in list(self
.tasks_running
):
496 task
.terminate(timeout
=timeout
)
498 def update_status(self
, new_status
):
499 assert new_status
in ["PASS", "FAIL", "UNKNOWN", "ERROR"]
501 if new_status
== "UNKNOWN":
504 if self
.status
== "ERROR":
507 if new_status
== "PASS":
508 assert self
.status
!= "FAIL"
511 elif new_status
== "FAIL":
512 assert self
.status
!= "PASS"
515 elif new_status
== "ERROR":
516 self
.status
= "ERROR"
521 def run(self
, setupmode
):
525 with
open(f
"{self.workdir}/config.sby", "r") as f
:
528 if mode
in ["options", "engines", "files"]:
529 line
= re
.sub(r
"\s*(\s#.*)?$", "", line
)
530 if line
== "" or line
[0] == "#":
535 if mode
is None and (len(line
) == 0 or line
[0] == "#"):
537 match
= re
.match(r
"^\s*\[(.*)\]\s*$", line
)
539 entries
= match
.group(1).split()
540 if len(entries
) == 0:
541 self
.error(f
"sby file syntax error: {line}")
543 if entries
[0] == "options":
545 if len(self
.options
) != 0 or len(entries
) != 1:
546 self
.error(f
"sby file syntax error: {line}")
549 if entries
[0] == "engines":
551 if len(self
.engines
) != 0 or len(entries
) != 1:
552 self
.error(f
"sby file syntax error: {line}")
555 if entries
[0] == "script":
557 if len(self
.script
) != 0 or len(entries
) != 1:
558 self
.error(f
"sby file syntax error: {line}")
561 if entries
[0] == "file":
563 if len(entries
) != 2:
564 self
.error(f
"sby file syntax error: {line}")
565 current_verbatim_file
= entries
[1]
566 if current_verbatim_file
in self
.verbatim_files
:
567 self
.error(f
"duplicate file: {entries[1]}")
568 self
.verbatim_files
[current_verbatim_file
] = list()
571 if entries
[0] == "files":
573 if len(entries
) != 1:
574 self
.error(f
"sby file syntax error: {line}")
577 self
.error(f
"sby file syntax error: {line}")
579 if mode
== "options":
580 entries
= line
.split()
581 if len(entries
) != 2:
582 self
.error(f
"sby file syntax error: {line}")
583 self
.options
[entries
[0]] = entries
[1]
586 if mode
== "engines":
587 entries
= line
.split()
588 self
.engines
.append(entries
)
592 self
.script
.append(line
)
596 entries
= line
.split()
597 if len(entries
) == 1:
598 self
.files
[os
.path
.basename(entries
[0])] = entries
[0]
599 elif len(entries
) == 2:
600 self
.files
[entries
[0]] = entries
[1]
602 self
.error(f
"sby file syntax error: {line}")
606 self
.verbatim_files
[current_verbatim_file
].append(raw_line
)
609 self
.error(f
"sby file syntax error: {line}")
611 self
.handle_str_option("mode", None)
613 if self
.opt_mode
not in ["bmc", "prove", "cover", "live"]:
614 self
.error(f
"Invalid mode: {self.opt_mode}")
616 self
.expect
= ["PASS"]
617 if "expect" in self
.options
:
618 self
.expect
= self
.options
["expect"].upper().split(",")
619 self
.used_options
.add("expect")
621 for s
in self
.expect
:
622 if s
not in ["PASS", "FAIL", "UNKNOWN", "ERROR", "TIMEOUT"]:
623 self
.error(f
"Invalid expect value: {s}")
625 self
.handle_bool_option("multiclock", False)
626 self
.handle_bool_option("wait", False)
627 self
.handle_int_option("timeout", None)
629 self
.handle_str_option("smtc", None)
630 self
.handle_int_option("skip", None)
631 self
.handle_str_option("tbtop", None)
633 if self
.opt_smtc
is not None:
634 for engine
in self
.engines
:
635 if engine
[0] != "smtbmc":
636 self
.error("Option smtc is only valid for smtbmc engine.")
638 if self
.opt_skip
is not None:
639 if self
.opt_skip
== 0:
642 for engine
in self
.engines
:
643 if engine
[0] not in ["smtbmc", "btor"]:
644 self
.error("Option skip is only valid for smtbmc and btor engines.")
646 if len(self
.engines
) == 0:
647 self
.error("Config file is lacking engine configuration.")
650 rmtree(f
"{self.workdir}/model", ignore_errors
=True)
658 if self
.opt_mode
== "bmc":
660 sby_mode_bmc
.run(self
)
662 elif self
.opt_mode
== "prove":
663 import sby_mode_prove
664 sby_mode_prove
.run(self
)
666 elif self
.opt_mode
== "live":
668 sby_mode_live
.run(self
)
670 elif self
.opt_mode
== "cover":
671 import sby_mode_cover
672 sby_mode_cover
.run(self
)
677 for opt
in self
.options
.keys():
678 if opt
not in self
.used_options
:
679 self
.error(f
"Unused option: {opt}")
683 total_clock_time
= int(time() - self
.start_clock_time
)
685 if os
.name
== "posix":
686 ru
= resource
.getrusage(resource
.RUSAGE_CHILDREN
)
687 total_process_time
= int((ru
.ru_utime
+ ru
.ru_stime
) - self
.start_process_time
)
688 self
.total_time
= total_process_time
691 "Elapsed clock time [H:MM:SS (secs)]: {}:{:02d}:{:02d} ({})".format
692 (total_clock_time
// (60*60), (total_clock_time
// 60) % 60, total_clock_time
% 60, total_clock_time
),
693 "Elapsed process time [H:MM:SS (secs)]: {}:{:02d}:{:02d} ({})".format
694 (total_process_time
// (60*60), (total_process_time
// 60) % 60, total_process_time
% 60, total_process_time
),
698 "Elapsed clock time [H:MM:SS (secs)]: {}:{:02d}:{:02d} ({})".format
699 (total_clock_time
// (60*60), (total_clock_time
// 60) % 60, total_clock_time
% 60, total_clock_time
),
700 "Elapsed process time unvailable on Windows"
703 for line
in self
.summary
:
704 self
.log(f
"summary: {line}")
706 assert self
.status
in ["PASS", "FAIL", "UNKNOWN", "ERROR", "TIMEOUT"]
708 if self
.status
in self
.expect
:
711 if self
.status
== "PASS": self
.retcode
= 1
712 if self
.status
== "FAIL": self
.retcode
= 2
713 if self
.status
== "UNKNOWN": self
.retcode
= 4
714 if self
.status
== "TIMEOUT": self
.retcode
= 8
715 if self
.status
== "ERROR": self
.retcode
= 16
717 with
open(f
"{self.workdir}/{self.status}", "w") as f
:
718 for line
in self
.summary
: