13from pathlib
import Path
14from typing
import Dict, List, Optional, Callable, IO, Union
17_thisdir = Path(__file__).parent
18CosimCollateralDir = _thisdir
19_SUPPORTED_SIMULATORS = (
"verilator",
"questa")
23 """Check if a TCP port is open locally."""
24 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
25 result = sock.connect_ex((
'127.0.0.1', port))
27 return True if result == 0
else False
31 """Return the simulator backends known to the ESI cosim runtime."""
32 return list(_SUPPORTED_SIMULATORS)
35def is_simulator_available(name: str) -> bool:
36 """Return True if the requested simulator backend is usable.
38 This checks the executable and environment conventions used by the Python
39 cosim backends, so pytest callers can skip simulator-backed tests before
40 generating or compiling hardware collateral.
43 if name ==
"verilator":
44 from .verilator
import Verilator
45 if Verilator._find_verilator_bin()
is None:
47 return Verilator._find_verilator_root()
is not None
49 return shutil.which(
"vsim")
is not None
50 raise ValueError(f
"Unknown simulator: {name}")
54 """Return the known simulator backends available in this environment."""
56 name
for name
in _SUPPORTED_SIMULATORS
if is_simulator_available(name)
64 self.user: List[Path] = []
66 self.dpi_so: List[str] = [
"EsiCosimDpiServer"]
68 self.dpi_sv: List[Path] = [
69 CosimCollateralDir /
"Cosim_DpiPkg.sv",
70 CosimCollateralDir /
"Cosim_Endpoint.sv",
71 CosimCollateralDir /
"Cosim_CycleCount.sv",
72 CosimCollateralDir /
"Cosim_Manifest.sv",
78 """Add a single RTL file to the source list."""
82 raise FileNotFoundError(f
"File {file} does not exist")
85 """Add all the RTL files in a directory to the source list. Files named in
86 the directory's `filelist.f` (as written by ExportVerilog) are added first,
87 in the listed order, since some simulators require dependencies such as
88 packages to be compiled before their users."""
89 listed: List[Path] = []
90 filelist = dir /
"filelist.f"
91 if filelist.is_file():
92 for line
in filelist.read_text().splitlines():
94 if line
and (dir / line).is_file():
95 listed.append(dir / line)
96 for file
in listed + sorted(dir.iterdir()):
99 if file.is_file()
and (file.suffix ==
".sv" or file.suffix ==
".v"):
105 """Return a list of all the DPI shared object files (the loadable
106 artifact: ``.so`` on POSIX, ``.dll`` on Windows)."""
107 return [self.
_find_dpi(name, link=
False)
for name
in self.dpi_so]
110 """Return a list of files to pass to the linker for the DPI libraries.
111 On POSIX this is the same as ``dpi_so_paths()``; on Windows it is the
112 import library (``.lib``) sitting next to the DLL."""
113 return [self.
_find_dpi(name, link=
True)
for name
in self.dpi_so]
116 is_windows = os.name ==
"nt"
118 def check_path(p: Path) -> Optional[Path]:
120 suffix =
".lib" if link
else ".dll"
121 cand = p / f
"{name}{suffix}"
123 cand = p / f
"lib{name}.so"
124 return cand
if cand.exists()
else None
126 env = Simulator.get_env()
128 search_env = env.get(
"PATH",
"")
131 search_env = env.get(
"LD_LIBRARY_PATH",
"")
133 for path
in search_env.split(env_sep):
136 p = check_path(Path(path))
145 _thisdir.parent /
"lib",
146 _thisdir.parent.parent /
"lib",
148 p = check_path(candidate)
152 suffix = (
".lib" if link
else ".dll")
if is_windows
else ".so"
153 raise FileNotFoundError(f
"Could not find {name}{suffix}")
157 """Return a list of all the RTL source files."""
158 return self.dpi_sv + self.user
164 proc: subprocess.Popen,
166 threads: Optional[List[threading.Thread]] =
None,
170 self.threads: List[threading.Thread] = threads
or []
174 """Make sure to stop the simulation no matter what."""
180 self.
proc.send_signal(signal.CTRL_BREAK_EVENT)
181 except (OSError, ValueError):
184 os.killpg(os.getpgid(self.
proc.pid), signal.SIGINT)
187 self.
proc.wait(timeout=1.0)
188 except subprocess.TimeoutExpired:
193 for t
in self.threads:
199 CompileCommand = List[str]
200 CompileFunction = Callable[[], Optional[int]]
201 CompileStep = Union[CompileCommand, CompileFunction]
209 sources: SourceFiles,
212 save_waveform: bool =
False,
213 run_stdout_callback: Optional[Callable[[str],
None]] =
None,
214 run_stderr_callback: Optional[Callable[[str],
None]] =
None,
215 compile_stdout_callback: Optional[Callable[[str],
None]] =
None,
216 compile_stderr_callback: Optional[Callable[[str],
None]] =
None,
217 make_default_logs: bool =
True,
218 macro_definitions: Optional[Dict[str, Optional[str]]] =
None):
219 """Simulator base class.
221 Optional sinks can be provided for capturing output. If not provided,
222 the simulator will write to log files in `run_dir`.
225 sources: SourceFiles describing RTL/DPI inputs.
226 run_dir: Directory where build/run artifacts are placed.
227 debug: Enable cosim debug mode.
228 save_waveform: When True and debug=True, dump simulator waveforms to a
229 waveform file. The exact format depends on the backend (e.g. FST for
230 Verilator, VCD for Questa). Requires debug to be enabled.
231 run_stdout_callback: Line-based callback for runtime stdout.
232 run_stderr_callback: Line-based callback for runtime stderr.
233 compile_stdout_callback: Line-based callback for compile stdout.
234 compile_stderr_callback: Line-based callback for compile stderr.
235 make_default_logs: If True and corresponding callback is not supplied,
236 create log file and emit via internally-created callback.
237 macro_definitions: Optional dictionary of macro definitions to be defined
247 self._default_files: List[IO[str]] = []
249 def _ensure_default(cb: Optional[Callable[[str],
None]], filename: str):
250 """Return (callback, file_handle_or_None) with optional file creation.
253 * If a callback is provided, return it unchanged with no file.
254 * If no callback and make_default_logs is False, return (None, None).
255 * If no callback and make_default_logs is True, create a log file and
256 return a writer callback plus the opened file handle.
260 if not make_default_logs:
263 p.parent.mkdir(parents=
True, exist_ok=
True)
265 self._default_files.
append(logf)
267 def _writer(line: str, _lf=logf):
268 _lf.write(line +
"\n")
275 compile_stdout_callback,
'compile_stdout.log')
277 compile_stderr_callback,
'compile_stderr.log')
279 run_stdout_callback,
'sim_stdout.log')
281 run_stderr_callback,
'sim_stderr.log')
285 """Get the environment variables to locate shared objects."""
287 env = os.environ.copy()
291 str(_thisdir.parent),
292 str(_thisdir.parent /
"lib"),
293 str(_thisdir.parent.parent /
"lib"),
300 env[
"PATH"] = sep.join(lib_dirs) + sep + env.get(
"PATH",
"")
302 env[
"LIBRARY_PATH"] = env.get(
"LIBRARY_PATH",
303 "") +
":" +
":".join(lib_dirs)
304 env[
"LD_LIBRARY_PATH"] = env.get(
"LD_LIBRARY_PATH",
305 "") +
":" +
":".join(lib_dirs)
309 """Return the compile steps for the simulator.
311 Each step may be either a shell command (`List[str]`) or a Python callback
312 (`Callable[[], Optional[int]]`). Python callbacks should return `0` or
313 `None` on success and a non-zero integer on failure.
315 assert False,
"Must be implemented by subclass"
319 env=Simulator.get_env(),
324 if isinstance(ret, int)
and ret != 0:
325 print(
"====== Compilation failure")
333 print(stdout_content)
338 print(stderr_content)
340 return ret
if isinstance(ret, int)
else 1
347 if not isinstance(ret, int):
348 raise TypeError(
"compile step callback must return int or None")
354 self.
run_dir.mkdir(parents=
True, exist_ok=
True)
362 """Return the command to run the simulation."""
363 assert False,
"Must be implemented by subclass"
367 """File extension for waveform dumps.
369 Subclasses should override if their format differs. The Verilator C++
370 driver writes FST (via ``VerilatedFstC``); the generic SV driver uses
371 ``$dumpfile/$dumpvars`` which produces VCD.
375 def run_proc(self, gui: bool =
False) -> SimProcess:
376 """Run the simulation process. Returns the Popen object and the port which
377 the simulation is listening on.
379 If user-provided stdout/stderr sinks were supplied in the constructor,
380 they are used. Otherwise, log files are created in `run_dir`.
382 self.
run_dir.mkdir(parents=
True, exist_ok=
True)
384 env_gui = os.environ.get(
"COSIM_GUI",
"0")
390 portFileName = self.
run_dir /
"cosim.cfg"
391 if os.path.exists(portFileName):
392 os.remove(portFileName)
395 simEnv = Simulator.get_env()
398 simEnv[
"COSIM_DEBUG_FILE"] = str(debug_file)
399 if "DEBUG_PERIOD" not in simEnv:
401 simEnv[
"DEBUG_PERIOD"] =
"1"
403 waveform_file = (self.
run_dir /
404 f
"cosim_waveform{self.waveform_extension}").
resolve()
405 simEnv[
"SAVE_WAVE"] = str(waveform_file)
418 while (
not os.path.exists(portFileName))
and \
422 if checkCount > 500
and not gui:
423 raise Exception(f
"Cosim never wrote cfg file: {portFileName}")
426 portFile = open(portFileName,
"r")
427 for line
in portFile.readlines():
428 m = re.match(
"port: (\\d+)", line)
430 port = int(m.group(1))
436 return SimProcess(proc=proc, port=port, threads=threads, gui=gui)
439 self, cmd: List[str], env: Optional[Dict[str, str]], cwd: Optional[Path],
440 stdout_cb: Optional[Callable[[str],
441 None]], stderr_cb: Optional[Callable[[str],
443 wait: bool) -> int | tuple[subprocess.Popen, List[threading.Thread]]:
444 """Start a subprocess and stream its stdout/stderr to callbacks.
446 If wait is True, blocks until process completes and returns its exit code.
447 If wait is False, returns the Popen object (threads keep streaming).
449 if os.name ==
"posix":
450 proc = subprocess.Popen(cmd,
451 stdout=subprocess.PIPE,
452 stderr=subprocess.PIPE,
456 preexec_fn=os.setsid)
458 proc = subprocess.Popen(cmd,
459 stdout=subprocess.PIPE,
460 stderr=subprocess.PIPE,
464 creationflags=subprocess.CREATE_NEW_PROCESS_GROUP)
466 def _reader(pipe, cb):
470 if raw.endswith(
'\n'):
475 except Exception
as e:
476 print(f
"Exception in simulator output callback: {e}")
478 threads: List[threading.Thread] = [
479 threading.Thread(target=_reader,
480 args=(proc.stdout, stdout_cb),
482 threading.Thread(target=_reader,
483 args=(proc.stderr, stderr_cb),
497 server_only: bool =
False) -> int:
498 """Start the simulation then run the command specified. Kill the simulation
499 when the command exits."""
509 f
"Running in server-only mode on port {simProc.port} - Press anything to kill the server..."
514 testEnv = os.environ.copy()
515 testEnv[
"ESI_COSIM_PORT"] = str(simProc.port)
516 testEnv[
"ESI_COSIM_HOST"] =
"localhost"
517 ret = subprocess.run(inner_command, cwd=os.getcwd(),
518 env=testEnv).returncode
520 print(
"GUI mode - waiting for simulator to exit...")
524 if simProc
and simProc.proc.poll()
is None:
528def load_macro_definitions(path: Path) -> Dict[str, Optional[str]]:
529 """Read RTL macro definitions from a JSON file.
531 The file must hold an object mapping macro name to value, where null defines
532 the macro without assigning one::
534 {"SPB_256BIT_DATA": "1", "SYNTHESIS": null}
536 This lets a source generator that only discovers its macros while running --
537 a build system reading its own project description, for instance -- hand them
538 to the simulator without the caller having to know them up front.
541 path: The JSON file to read.
544 The macro mapping, suitable for `get_simulator`'s `macro_definitions`.
547 FileNotFoundError: If *path* does not exist.
548 ValueError: If *path* is not a JSON object of name/value pairs.
550 if not path.is_file():
551 raise FileNotFoundError(f
"Macro definitions file not found: {path}")
554 definitions = json.loads(path.read_text())
555 except ValueError
as e:
556 raise ValueError(f
"{path} is not valid JSON: {e}")
from e
558 if not isinstance(definitions, dict):
559 raise ValueError(f
"{path} must contain a JSON object mapping macro name "
560 f
"to value, got {type(definitions).__name__}")
562 macros: Dict[str, Optional[str]] = {}
563 for name, value
in definitions.items():
564 if value
is not None and not isinstance(value, (str, int, float, bool)):
565 raise ValueError(f
"{path}: macro '{name}' has a "
566 f
"{type(value).__name__} value; expected a scalar or "
568 macros[str(name)] =
None if value
is None else str(value)
574 sources: SourceFiles,
577 save_waveform: bool =
False,
578 macro_definitions: Optional[Dict[str, Optional[str]]] =
None) -> Simulator:
579 """Create a simulator backend.
582 name: Simulator backend name.
583 sources: SourceFiles describing RTL/DPI inputs.
584 rundir: Directory where build/run artifacts are placed.
585 debug: Enable cosim debug mode.
586 save_waveform: Save simulator waveforms when debug mode is enabled.
587 macro_definitions: Optional mapping of macro names to values. A value of
588 None defines the macro without assigning a value.
591 if name ==
"verilator":
592 from .verilator
import Verilator
593 return Verilator(sources,
596 save_waveform=save_waveform,
597 macro_definitions=macro_definitions)
598 elif name ==
"questa":
599 from .questa
import Questa
600 return Questa(sources,
603 save_waveform=save_waveform,
604 macro_definitions=macro_definitions)
606 raise ValueError(f
"Unknown simulator: {name}")
static void print(TypedAttr val, llvm::raw_ostream &os)
static mlir::Operation * resolve(Context &context, mlir::SymbolRefAttr sym)
static StringAttr append(StringAttr base, const Twine &suffix)
Return a attribute with the specified suffix appended.
__init__(self, subprocess.Popen proc, int port, Optional[List[threading.Thread]] threads=None, bool gui=False)
int|tuple[subprocess.Popen, List[threading.Thread]] _start_process_with_callbacks(self, List[str] cmd, Optional[Dict[str, str]] env, Optional[Path] cwd, Optional[Callable[[str], None]] stdout_cb, Optional[Callable[[str], None]] stderr_cb, bool wait)
__init__(self, SourceFiles sources, Path run_dir, bool debug, bool save_waveform=False, Optional[Callable[[str], None]] run_stdout_callback=None, Optional[Callable[[str], None]] run_stderr_callback=None, Optional[Callable[[str], None]] compile_stdout_callback=None, Optional[Callable[[str], None]] compile_stderr_callback=None, bool make_default_logs=True, Optional[Dict[str, Optional[str]]] macro_definitions=None)
List[CompileStep] compile_commands(self)
int _run_compile_command(self, CompileCommand cmd)
str waveform_extension(self)
int _run_compile_step(self, CompileStep step)
List[str] run_command(self, bool gui)
SimProcess run_proc(self, bool gui=False)
List[Path] rtl_sources(self)
List[Path] dpi_link_paths(self)
add_file(self, Path file)
Path _find_dpi(self, str name, bool link)
None __init__(self, str top)
List[Path] dpi_so_paths(self)
List[str] supported_simulators()
List[str] available_simulators()