Skip to content
VLSI Mentor

Python Part II · Module 23 · Running EDA Tools from Python

Driving VCS, Xcelium, Questa and Verdi

This module has built the pieces one at a time: the list form, the return code, the log file, the run directory, the timeout, the launch handler, the recorded command. A real flow has to assemble them — and then survive the request that arrives six months later, which is to run the same regression on a different simulator. That request is where most flows are discovered to be a VCS flow with a configuration file rather than a flow that happens to use VCS, and the difference is one design decision about where the tool-specific part stops.

Advanced20 min readPythonPart IIAutomationEDA ToolsVerification Flow

Part II · Module 23 · Chapter 23.11 · Driving VCS, Xcelium, Questa and Verdi

1. Three Simulators, Three Command Shapes

The same job — compile a design and run one test with a seed — in three vendors' spellings:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
TEST, SEED = "axi_smoke", 1839274

xcelium = ["xrun", "-64bit", "-f", "files.f", "-top", "tb_top",
           "+UVM_TESTNAME=" + TEST, "+ntb_random_seed=" + str(SEED),
           "-l", "sim.log"]

vcs_build = ["vcs", "-full64", "-sverilog", "-f", "files.f", "-top", "tb_top",
             "-l", "compile.log", "-o", "simv"]
vcs_run = ["./simv", "+UVM_TESTNAME=" + TEST, "+ntb_random_seed=" + str(SEED),
           "-l", "sim.log"]

questa_build = ["vlog", "-sv", "-f", "files.f", "-l", "compile.log"]
questa_run = ["vsim", "-c", "tb_top", "-do", "run -all; quit -f",
              "+UVM_TESTNAME=" + TEST, "-sv_seed", str(SEED), "-l", "sim.log"]

for label, steps in (("xcelium", [xcelium]),
                     ("vcs", [vcs_build, vcs_run]),
                     ("questa", [questa_build, questa_run])):
    print(f"{label:<8} {len(steps)} invocation(s)")
    for step in steps:
        print(f"           {step[0]:<8} {len(step):>2} arguments")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
xcelium  1 invocation(s)
           xrun     10 arguments
vcs      2 invocation(s)
           vcs      11 arguments
           ./simv    5 arguments
questa   2 invocation(s)
           vlog      6 arguments
           vsim     10 arguments

Look past the flag spellings at the first column of that output. The three tools do not have the same shape.

xrun is one invocation: it elaborates and runs in a single call. vcs is two — vcs builds a binary called simv, and then you run ./simv, which is a different executable with entirely different arguments. Questa is also two, vlog then vsim, and vsim takes its commands through a -do string.

The seed is spelled three ways — +ntb_random_seed, the same on ./simv, and -sv_seed — and the test is passed as a plusarg in all three. Those differences are the easy half. The invocation count is the hard half, and it is the one that decides whether a design works.

2. The Seam in the Wrong Place

Here is the design almost everybody writes first, and what happens to it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def build_cmd(sim, test, seed):
    """One function, a branch per simulator."""
    if sim == "xrun":
        return ["xrun", "-64bit", "-f", "files.f", "-top", "tb_top",
                f"+UVM_TESTNAME={test}", f"+ntb_random_seed={seed}", "-l", "sim.log"]
    if sim == "vcs":
        return ["vcs", "-full64", "-sverilog", "-f", "files.f", "-top", "tb_top",
                "-l", "compile.log", "-o", "simv"]
    raise ValueError(f"unknown simulator: {sim}")


print("xrun:", " ".join(build_cmd("xrun", "axi_smoke", 1839274)[:3]), "...")
print("vcs :", " ".join(build_cmd("vcs", "axi_smoke", 1839274)[:3]), "...")
print()
print("but the vcs command never received the test or the seed:")
cmd = build_cmd("vcs", "axi_smoke", 1839274)
print("   contains axi_smoke:", any("axi_smoke" in part for part in cmd))
print("   contains the seed :", any("1839274" in part for part in cmd))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
xrun: xrun -64bit -f ...
vcs : vcs -full64 -sverilog ...

but the vcs command never received the test or the seed:
   contains axi_smoke: False
   contains the seed : False

The function returns one command, so the VCS branch can only return the build — and the test and the seed, which belong to the second invocation, are silently dropped. The signature was fixed before the shapes were understood.

The usual next move is a flag: build_cmd(sim, test, seed, phase="run"). That works, and it puts the knowledge that VCS has two phases into every caller. Six months later the flow has if sim == "vcs" in four places, and the request to add a fourth simulator touches all of them.

The seam has to go where the tools actually differ. They differ in how many invocations a run takes, so that is what an adapter has to be allowed to decide.

3. An Adapter per Tool

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def xrun_cmd(test, seed, waves):
    """Xcelium: one invocation elaborates and runs."""
    cmd = ["xrun", "-64bit", "-f", "files.f", "-top", "tb_top",
           f"+UVM_TESTNAME={test}", f"+ntb_random_seed={seed}", "-l", "sim.log"]
    if waves:
        cmd += ["-input", "waves.tcl"]
    return cmd


def vcs_cmd(test, seed, waves):
    """VCS: vcs builds ./simv, then ./simv runs the test."""
    build = ["vcs", "-full64", "-sverilog", "-f", "files.f", "-top", "tb_top",
             "-l", "compile.log", "-o", "simv"]
    if waves:
        build += ["-debug_access+all"]
    run = ["./simv", f"+UVM_TESTNAME={test}", f"+ntb_random_seed={seed}", "-l", "sim.log"]
    return [build, run]


def questa_cmd(test, seed, waves):
    """Questa: vlog compiles, vsim runs with a -do script."""
    build = ["vlog", "-sv", "-f", "files.f", "-l", "compile.log"]
    do = "run -all; quit -f"
    run = ["vsim", "-c", "tb_top", "-do", do,
           f"+UVM_TESTNAME={test}", "-sv_seed", str(seed), "-l", "sim.log"]
    if waves:
        run.insert(2, "-voptargs=+acc")
    return [build, run]


ADAPTERS = {"vcs": vcs_cmd, "xrun": xrun_cmd, "vsim": questa_cmd}

cmd = ADAPTERS["xrun"]("axi_smoke", 1839274, False)
print("xrun  ->", type(cmd).__name__, "of", type(cmd[0]).__name__)

cmd = ADAPTERS["vcs"]("axi_smoke", 1839274, False)
print("vcs   ->", type(cmd).__name__, "of", type(cmd[0]).__name__)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
xrun  -> list of str
vcs   -> list of list

That is the canonical annotation:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ADAPTERS = {"vcs": vcs_cmd, "xrun": xrun_cmd, "vsim": questa_cmd}
cmd = ADAPTERS[sim](test, seed, waves)

A dictionary whose values are functions. That is worth pausing on if it is new: a function name without parentheses is just a name bound to the function object, exactly like any other value, so it can be stored in a dict, passed as an argument or returned. ADAPTERS[sim] looks one up and (test, seed, waves) calls it — the lookup and the call are two separate steps, and there is no if anywhere in the flow.

Chapter 9.8 posed this same problem — dispatching on a simulator name once a chain of elif branches stops reading well — and answered it with match. A dict of functions is the other answer, and it has one property match does not: the set of supported simulators is data, so it can be listed, counted and extended without touching the dispatch site at all. Section 7 is that property being measured.

But look at the two return types in the output. xrun_cmd returns a list of strings — one command. vcs_cmd returns a list of lists — two. The annotation's single cmd is exactly right for the first and ambiguous for the second, and that ambiguity will reach subprocess.run as a TypeError on the day somebody switches simulator.

4. Always a List of Steps

The fix is to make every adapter return the same thing, which means the general shape, not the common one:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def normalise(result):
    """An adapter may return one command or several. Always yield a list of commands."""
    if result and isinstance(result[0], str):
        return [result]
    return list(result)


def xrun_cmd(test, seed, waves):
    return ["xrun", "-64bit", "-f", "files.f", "-top", "tb_top",
            f"+UVM_TESTNAME={test}", f"+ntb_random_seed={seed}", "-l", "sim.log"]


def vcs_cmd(test, seed, waves):
    return [["vcs", "-full64", "-sverilog", "-f", "files.f", "-top", "tb_top",
             "-l", "compile.log", "-o", "simv"],
            ["./simv", f"+UVM_TESTNAME={test}", f"+ntb_random_seed={seed}", "-l", "sim.log"]]


def questa_cmd(test, seed, waves):
    return [["vlog", "-sv", "-f", "files.f", "-l", "compile.log"],
            ["vsim", "-c", "tb_top", "-do", "run -all; quit -f",
             f"+UVM_TESTNAME={test}", "-sv_seed", str(seed), "-l", "sim.log"]]


ADAPTERS = {"vcs": vcs_cmd, "xrun": xrun_cmd, "vsim": questa_cmd}

for sim in sorted(ADAPTERS):
    steps = normalise(ADAPTERS[sim]("axi_smoke", 1839274, False))
    names = " then ".join(step[0] for step in steps)
    print(f"{sim:<6} {len(steps)} step(s): {names}")
    print(f"       every step is a list of str: "
          f"{all(isinstance(p, str) for step in steps for p in step)}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
vcs    2 step(s): vcs then ./simv
       every step is a list of str: True
vsim   2 step(s): vlog then vsim
       every step is a list of str: True
xrun   1 step(s): xrun
       every step is a list of str: True

One uniform type: a list of commands, each a list of strings. The one-invocation tool returns a list of one, which costs nothing and means the flow has a single case to handle.

normalise is three lines and it is the whole generalisation. It also keeps the canonical annotation's convenience — an adapter author writing a single-invocation tool can return a bare command and it still works.

5. The Shape of the Runner

Three blocks in a row joined by two arrows. On the left, the runner: the run directory, the timeout, the launch handling, the recorded command and the status, which is written once and names no simulator. In the middle, the adapter table, mapping each simulator name to one function, which is the only place a vendor's flag spelling appears. On the right, the steps: a list of commands, each a list of strings, which the runner executes the same way whichever adapter produced them. An arrow labelled sim name runs from the runner to the adapter table, and an arrow labelled builds runs from the adapter table out to the steps.The runnercwd, timeout, record,statusADAPTERSone function per toolStepslists of stringssim namebuilds12
Figure 1 — the seam. The flow logic on the left is written once and never names a simulator; the adapter table in the middle is the only place a vendor's spelling appears; the steps on the right are plain lists of strings that the runner executes identically whichever tool produced them.

Everything on the left of that diagram is the same for every simulator, and everything a vendor cares about is in the middle. The test of the design is whether that is actually true, and Section 7 measures it.

6. The Runner

Azvya Education Pvt. Ltd.VLSI Mentor
fake_tool.py
import os
import sys

# A stand-in for a simulator: writes a log, maybe a binary, exits with a code.
name = os.path.basename(sys.argv[0])
code = int(os.environ.get("FAKE_RC", "0"))
with open("sim.log", "a", encoding="utf-8") as handle:
    handle.write(f"{name}: started with {len(sys.argv) - 1} arguments\n")
    if code:
        handle.write("UVM_ERROR drv.sv(88) @ 141900: response mismatch\n")
    handle.write("TEST DONE\n")
open("simv", "w").write("binary\n")
sys.exit(code)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import json
import os
import shlex
import shutil
import subprocess
import sys
from pathlib import Path

TOOL = Path("fake_tool.py").resolve()


class ToolError(Exception):
    """The tool could not be launched — a setup failure, not a tool failure."""


def normalise(result):
    if result and isinstance(result[0], str):
        return [result]
    return list(result)


def xrun_cmd(test, seed, waves):
    return [sys.executable, str(TOOL), "-64bit", "-f", "files.f",
            f"+UVM_TESTNAME={test}", f"+ntb_random_seed={seed}", "-l", "sim.log"]


def vcs_cmd(test, seed, waves):
    return [[sys.executable, str(TOOL), "-full64", "-sverilog", "-o", "simv"],
            [sys.executable, str(TOOL), f"+UVM_TESTNAME={test}",
             f"+ntb_random_seed={seed}", "-l", "sim.log"]]


ADAPTERS = {"vcs": vcs_cmd, "xrun": xrun_cmd}
TIMEOUTS = {"axi_smoke": 600, "axi_soak": 28800}
DEFAULT_TIMEOUT_S = 3600


def run_one(sim, test, seed, waves=False, rc=0):
    """One test, one simulator, every lesson in this module."""
    if sim not in ADAPTERS:
        raise ToolError(f"unknown simulator '{sim}' — known: {sorted(ADAPTERS)}")

    run_dir = Path("runs") / f"{sim}_{test}_{seed}"
    run_dir.mkdir(parents=True, exist_ok=True)

    steps = normalise(ADAPTERS[sim](test, seed, waves))
    limit = TIMEOUTS.get(test, DEFAULT_TIMEOUT_S)

    (run_dir / "cmd.sh").write_text(
        "#!/bin/sh\n# written by the flow\n"
        + "".join("exec " + shlex.join(step) + "\n" if i == len(steps) - 1
                  else shlex.join(step) + " || exit $?\n"
                  for i, step in enumerate(steps)),
        encoding="utf-8")
    os.chmod(run_dir / "cmd.sh", 0o755)

    env = dict(os.environ, FAKE_RC=str(rc))
    record = {"sim": sim, "test": test, "seed": seed, "steps": len(steps),
              "run_dir": str(run_dir), "log": str(run_dir / "sim.log"),
              "limit_s": limit}

    for index, step in enumerate(steps):
        try:
            with open(run_dir / "sim.log", "a", encoding="utf-8") as log:
                cp = subprocess.run(step, cwd=run_dir, stdout=log,
                                    stderr=subprocess.STDOUT, timeout=limit, env=env)
        except FileNotFoundError as exc:
            raise ToolError(f"'{step[0]}' not found while running {test}") from exc
        except subprocess.TimeoutExpired:
            record.update(status="NO_FINISH", reason="TIMEOUT", rc=None,
                          failed_step=index)
            return record

        if cp.returncode != 0:
            record.update(status="FAIL", rc=cp.returncode, failed_step=index)
            return record

    record.update(status="PASS", rc=0, failed_step=None)
    return record


Path("files.f").write_text("rtl/alu.sv\n", encoding="utf-8")

for sim, test, rc in (("xrun", "axi_smoke", 0), ("vcs", "axi_smoke", 0),
                      ("vcs", "axi_reset", 1), ("xrun", "axi_soak", 0)):
    record = run_one(sim, test, 1839274, rc=rc)
    print(f"{record['sim']:<5} {record['test']:<10} {record['status']:<9} "
          f"steps={record['steps']} failed_step={record['failed_step']} "
          f"limit={record['limit_s']}")

try:
    run_one("vsim", "axi_smoke", 1)
except ToolError as exc:
    print("ToolError:", exc)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
xrun  axi_smoke  PASS      steps=1 failed_step=None limit=600
vcs   axi_smoke  PASS      steps=2 failed_step=None limit=600
vcs   axi_reset  FAIL      steps=2 failed_step=0 limit=3600
xrun  axi_soak   PASS      steps=1 failed_step=None limit=28800
ToolError: unknown simulator 'vsim' — known: ['vcs', 'xrun']

Count what that function contains and where each piece came from.

The list form and shell=False by default — Chapters 23.2 and 23.3. The return code checked per step, and the loop stopping at the first non-zero — Chapter 23.4. The tool's output appended to a log file rather than captured — Chapter 23.5. A run directory per simulator, test and seed, with cwd= and mkdir(parents=True) — Chapter 23.7. A timeout per test with a default — Chapter 23.8. FileNotFoundError converted to ToolError with from exc — Chapter 23.9. cmd.sh via shlex.join, with exec on the last step only — Chapter 23.10. And a record carrying the status, the log, the run directory and the limit — Chapters 20.11 and 21.1.

Three decisions in it are worth arguing for explicitly.

failed_step is in the record. With two-invocation tools, "it failed" is ambiguous — a failure at step 0 is a compile error and at step 1 is a test failure, which Chapter 20.11 would classify differently and which send people to completely different places. The index is the cheapest way to say which.

The loop stops at the first failure. Running ./simv after vcs failed produces a second, confusing error about a missing binary on top of the real one. Chapter 23.4's point that a non-zero code is a decision, not a log line.

cmd.sh uses || exit $? between steps and exec only on the last. Pasted by hand, it stops where the flow stopped, and the final exec makes the last tool's status the script's own — Chapter 23.10 Section 5. A naive cmd.sh listing both commands unconditionally reproduces neither the sequencing nor the exit status.

7. The Test: Add a Fourth Simulator

A design claim is worth what its measurement is worth, so here is the measurement.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def xrun_cmd(test, seed, waves):
    return ["xrun", "-64bit", f"+UVM_TESTNAME={test}", f"+ntb_random_seed={seed}"]


def vcs_cmd(test, seed, waves):
    return [["vcs", "-full64", "-o", "simv"],
            ["./simv", f"+UVM_TESTNAME={test}", f"+ntb_random_seed={seed}"]]


def questa_cmd(test, seed, waves):
    return [["vlog", "-sv", "-f", "files.f"],
            ["vsim", "-c", "tb_top", "-do", "run -all; quit -f",
             f"+UVM_TESTNAME={test}", "-sv_seed", str(seed)]]


# --- the fourth simulator: everything new is below this line ---
def verilator_cmd(test, seed, waves):
    """Verilator: build a C++ model, then run the produced binary."""
    build = ["verilator", "--binary", "-f", "files.f", "--top-module", "tb_top",
             "--Mdir", "obj_dir"]
    if waves:
        build += ["--trace"]
    return [build, ["./obj_dir/Vtb_top", f"+UVM_TESTNAME={test}", f"+verilator+seed+{seed}"]]


ADAPTERS = {"vcs": vcs_cmd, "xrun": xrun_cmd, "vsim": questa_cmd,
            "verilator": verilator_cmd}
# --- nothing above this line changed ---


def normalise(result):
    if result and isinstance(result[0], str):
        return [result]
    return list(result)


print(f"{'sim':<10} {'steps':>5}  first argv[0]      last argv[0]")
for sim in sorted(ADAPTERS):
    steps = normalise(ADAPTERS[sim]("axi_smoke", 1839274, False))
    print(f"{sim:<10} {len(steps):>5}  {steps[0][0]:<18} {steps[-1][0]}")

print()
print("adapters           :", len(ADAPTERS))
print("flow functions     :", "run_one, normalise")
print("flow lines changed :", 0)
print("new adapter returns:", "2 steps, same uniform type as the others")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sim        steps  first argv[0]      last argv[0]
vcs            2  vcs                ./simv
verilator      2  verilator          ./obj_dir/Vtb_top
vsim           2  vlog               vsim
xrun           1  xrun               xrun

adapters           : 4
flow functions     : run_one, normalise
flow lines changed : 0
new adapter returns: 2 steps, same uniform type as the others

Zero lines of flow logic changed. That number is the claim — not that the design is elegant, but that extending it costs one function in one place.

Verilator is a good fourth case precisely because it is the least like the other three: it compiles to a C++ model, puts it in a directory of its own, and the thing you run afterwards has a name derived from the top module. None of that reached the runner, because the runner only ever asked for a list of steps.

And the one-step case still works. xrun returns a bare command and normalise wraps it, so the simplest adapter stayed the simplest to write.

8. What Does Not Belong in an Adapter

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
FLOW_CONCERNS = [
    ("the run directory", "every tool needs one; the policy is the same"),
    ("the timeout", "a property of the test, not of the simulator"),
    ("launch failure handling", "the same two exceptions for every tool"),
    ("writing cmd.sh", "identical given a list of steps"),
    ("status classification", "PASS / FAIL / NO_FINISH / ERROR is the flow's vocabulary"),
    ("the record", "one schema, or nothing can compare two runs"),
]
ADAPTER_CONCERNS = [
    ("flag spelling", "-full64 versus -64bit"),
    ("how the seed is passed", "+ntb_random_seed versus -sv_seed"),
    ("how many invocations", "one, or build-then-run"),
    ("what the built artefact is called", "simv, or obj_dir/Vtb_top"),
    ("how waves are enabled", "-debug_access+all versus --trace"),
]

print("belongs in the FLOW (written once):")
for name, why in FLOW_CONCERNS:
    print(f"   {name:<26} {why}")
print()
print("belongs in an ADAPTER (per tool):")
for name, why in ADAPTER_CONCERNS:
    print(f"   {name:<26} {why}")
print()
print("adapter does:", "build argument lists")
print("adapter does not:", "run anything, time anything, classify anything")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
belongs in the FLOW (written once):
   the run directory          every tool needs one; the policy is the same
   the timeout                a property of the test, not of the simulator
   launch failure handling    the same two exceptions for every tool
   writing cmd.sh             identical given a list of steps
   status classification      PASS / FAIL / NO_FINISH / ERROR is the flow's vocabulary
   the record                 one schema, or nothing can compare two runs

belongs in an ADAPTER (per tool):
   flag spelling              -full64 versus -64bit
   how the seed is passed     +ntb_random_seed versus -sv_seed
   how many invocations       one, or build-then-run
   what the built artefact is called simv, or obj_dir/Vtb_top
   how waves are enabled      -debug_access+all versus --trace

adapter does: build argument lists
adapter does not: run anything, time anything, classify anything

The fourth line of the adapter list runs past its column, because one of those names is longer than the width — a reminder that a fixed width is a claim about your data, which Chapter 4.13's width and alignment control makes easy to set and easy to get wrong.

The rule underneath both lists is a single sentence: an adapter builds argument lists and does nothing else.

The moment an adapter calls subprocess.run itself, four things happen. Each new tool re-implements the timeout and gets it slightly different. The cmd.sh from one adapter and another drift apart. A fix to the launch handler has to be applied n times. And testing an adapter now requires the tool to exist, where a list-building function can be tested by asserting on the list — which is the property that made Section 7's measurement possible at all.

9. Predict, Run, Observe

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def one(test, seed, waves):
    return ["xrun", f"+UVM_TESTNAME={test}"]


def two(test, seed, waves):
    return [["vcs", "-o", "simv"], ["./simv", f"+ntb_random_seed={seed}"]]


def normalise(result):
    if result and isinstance(result[0], str):
        return [result]
    return list(result)


ADAPTERS = {"xrun": one, "vcs": two}

# A
print("A:", type(one("t", 1, False)[0]).__name__, type(two("t", 1, False)[0]).__name__)

# B
print("B:", len(normalise(one("t", 1, False))), len(normalise(two("t", 1, False))))

# C
print("C:", all(isinstance(p, str)
                for sim in ADAPTERS
                for step in normalise(ADAPTERS[sim]("t", 1, False))
                for p in step))

# D
print("D:", normalise(two("t", 1839274, False))[-1][-1])

# E
print("E:", callable(ADAPTERS["vcs"]), ADAPTERS["vcs"] is two)

# F
try:
    ADAPTERS["vsim"]("t", 1, False)
except KeyError as exc:
    print("F:", type(exc).__name__, exc)

# G
print("G:", sorted(ADAPTERS))

The verified output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A: str list
B: 1 2
C: True
D: +ntb_random_seed=1839274
E: True True
F: KeyError 'vsim'
G: ['vcs', 'xrun']

A — the two adapters return different shapes: a list of str and a list of list.

B — normalise makes both a list of steps, of length 1 and 2.

C — and every step is a list of strings, which is the uniform type the runner depends on.

D — the seed reached the second invocation, which the Section 2 design could not express.

E — the dictionary values are the functions themselves, not calls to them.

F — an unknown simulator raises KeyError, which is why Section 6's runner checks membership and raises ToolError with the known names instead.

G — the known simulators are just the dictionary's keys, which is what makes that error message free.

10. Exercises

Exercise 1 — three shapes. For the simulators you have access to, write out the full command for one test with a seed and count the invocations each needs.

Exercise 2 — the spelling table. Tabulate how each passes the test name, the seed and the filelist.

Exercise 3 — the wrong seam. Write the single-if version and show that the two-invocation simulator silently loses the seed.

Exercise 4 — the phase flag. Add a phase= parameter instead and say what knowledge that pushes onto every caller.

Exercise 5 — adapters. Write adapters for two simulators, one single-invocation and one build-then-run.

Exercise 6 — the dispatch table. Build ADAPTERS and call one by name with no if anywhere.

Exercise 7 — the two return types. Show the shapes differ, and say what reaches subprocess.run if nothing normalises them.

Exercise 8 — normalise. Write it in three lines and confirm both adapters produce a list of lists of strings.

Exercise 9 — the general shape. Argue in three sentences for returning a list of one over returning one command.

Exercise 10 — assemble. Write a run_one using every piece from this module and name the chapter each came from.

Exercise 11 — failed_step. Say what a failure at step 0 means versus step 1, and where each sends somebody.

Exercise 12 — stop early. Explain what happens if the loop continues after a failed build.

Exercise 13 — the cmd.sh for two steps. Write it with || exit $? and a final exec, and say what each buys.

Exercise 14 — measure the extension. Add a fourth simulator to your own runner and count the flow lines you changed. Report the number.

Exercise 15 — the least similar tool. Pick the tool least like your others and say which of its differences reached your flow.

Exercise 16 — draw the line. Classify ten concerns from your own flow as adapter or flow. Defend the two hardest.

Exercise 17 — what an adapter must not do. Say what breaks when an adapter calls subprocess.run itself, and name four consequences.

Exercise 18 — testable without the tool. Write a test that asserts on an adapter's returned list and say why it needs no simulator.

Exercise 19 — the second adapter. Sketch the interface for a log-interpretation adapter, and say which of Module 20's patterns are UVM's rather than a vendor's.

11. Summary

  • Three simulators are three command shapes, and the flag spellings are the easy half.
  • They differ in the number of invocations: xrun is one; VCS is vcs then ./simv; Questa is vlog then vsim.
  • A single-command signature cannot express that, so the two-invocation tool silently loses the test and the seed.
  • A phase= flag works and spreads the knowledge that one tool has two phases into every caller.
  • Put the seam where the tools actually differ, which is the invocation count, not the flag spelling.
  • ADAPTERS is a dict of functions — a name without parentheses is a value — so ADAPTERS[sim](...) replaces every if, and Chapter 9.8's dispatch problem gets an answer where the supported set is data.
  • Make every adapter return a list of steps, each a list of strings; a normalise of three lines lets single-invocation adapters stay simple.
  • Prefer the general shape over the common one — a function returning sometimes one thing and sometimes many pushes a type check onto every caller.
  • The runner holds everything from this module: list form, shell=False, per-step return codes, a log file, cwd=, timeout=, ToolError, cmd.sh, and the record.
  • Record failed_step — step 0 is a compile error and step 1 is a test failure, and they send people to different places.
  • Stop at the first failing step, or the real error arrives buried under a second one about a missing binary.
  • cmd.sh needs || exit $? between steps and exec on the last, so a pasted run sequences and exits like the flow did.
  • Adding a fourth simulator changed zero lines of flow logic, and that zero is the design's only real claim.
  • An adapter builds argument lists and nothing else. Running, timing, classifying and recording are the flow's.
  • An adapter that runs things cannot be tested without the tool, where a list-building function can be tested by asserting on the list.
  • What is separated here is command construction. Log interpretation differs between vendors too, and deserves an adapter of its own.

That completes Module 23. You can now launch a tool from Python safely, read what it reports, bound how long it takes, survive the failures that happen before it starts, record exactly what ran, and do all of it without naming a simulator in the flow itself.

What the module has deliberately not settled is the gap Chapter 23.10 Section 7 named: a command says which tool only in the sense of a name, and which binary that name finds is decided by PATH, by tool homes, by a licence variable and by whichever version happens to be installed. The next module is about that half — the environment a run actually used, and how to pass it deliberately instead of inheriting it by accident.

Where this fits

Part of the Python curriculum.