Source code for elasticai.creator.vhdl.ghdl_simulation
import glob
import os
import subprocess
from .ghdl_report_parsing import parse_report
[docs]
class SimulationError(Exception):
pass
[docs]
class GHDLSimulator:
"""Run a simulation tool for a given `top_design` and save whatever is written to stdout
for subsequent inspection.
This runner uses the GHDL tool.
The parsed content has the following keys: `("source", "line", "column", "time", "type", "content")'
Will raise a `SimulationError` in case any of the calls to ghdl in the steps `initialize` or `run` fails.
Args:
workdir: typically the path to your build root, this is where we will look for vhd files
"""
def __init__(self, workdir, top_design_name) -> None:
self._root = workdir
self._ghdl_dir = "ghdl_build"
self._files = list(glob.glob("**/*.vhd", root_dir=self._root, recursive=True))
self._standard = "08"
self._test_bench_name = top_design_name
self._generics: dict[str, str] = {}
self._error_message = ""
self._completed_process: None | subprocess.CompletedProcess = None
[docs]
def add_generic(self, **kwargs):
self._generics.update(kwargs)
[docs]
def initialize(self):
"""Call this function once before calling `run()` and on every file change."""
os.makedirs(f"{self._root}/{self._ghdl_dir}", exist_ok=True)
self._load_files()
self._compile()
[docs]
def run(self):
"""Runs the simulation and saves whatever the tool wrote to stdout.
You're supposed to call `initialize` once, before calling `run`."""
generic_options = [f"-g{key}={value}" for key, value in self._generics.items()]
self._execute_command(
self._assemble_command(["-r"])
+ ["-fsynopsys", self._test_bench_name]
+ generic_options
)
@property
def _result(self) -> str:
return self._stdout()
[docs]
def getReportedContent(self) -> list[str]:
"""Strips any information that the simulation tool added automatically to the output
to return only the information that was printed to stdout via VHDL/Verilog statements.
"""
parsed = parse_report(self._result)
return list(line["content"] for line in parsed)
[docs]
def getFullReport(self) -> list[dict]:
"""Parses the output from the simulation tool, to provide a more structured representation.
The exact content depends on the simulation tool.
"""
return parse_report(self._result)
[docs]
def getRawResult(self) -> str:
"""Returns the raw stdout output as written by the simulation tool."""
return self._result
def _load_files(self):
self._execute_command(self._assemble_command("-i") + self._files)
def _compile(self):
self._execute_command(
self._assemble_command("-m") + ["-fsynopsys", self._test_bench_name]
)
def _execute_command(self, command):
try:
self._completed_process = subprocess.run(
command, cwd=self._root, capture_output=True, check=True
)
except subprocess.CalledProcessError as e:
raise SimulationError(
f"ERROR:: executing {command}\n\tSTDERR::"
f" {e.stderr.decode()}\n\tSTDOUT:: {e.stdout.decode()}"
)
self._check_for_error()
def _check_for_error(self):
try:
self._completed_process.check_returncode()
except subprocess.CalledProcessError as exception:
error_message = self._get_error_message()
sim_error = SimulationError(error_message)
raise sim_error from exception
def _get_error_message(self) -> str:
# ghdl seems to pipe errors to stdout instead of stdin
error_message = self._stderr()
if error_message == "":
error_message = self._stdout()
return error_message
def _stdout(self) -> str:
if self._completed_process is not None:
return self._completed_process.stdout.decode()
else:
return ""
def _stderr(self) -> str:
if self._completed_process is not None:
return self._completed_process.stderr.decode()
else:
return ""
def _assemble_command(self, command_flags):
if isinstance(command_flags, str):
command_flags = [command_flags]
return (
["ghdl"] + command_flags + [f"--std={self._standard}", self._workdir_flag]
)
@property
def _workdir_flag(self):
return f"--workdir={self._ghdl_dir}"