Skip to content
VLSI Mentor

Python Part II · Module 25 · Simulation Automation

Running One Test With a Seed and Plusargs

Step four finally runs something. The mechanics are entirely Chapter 23's — a command list, a log file, a working directory, a timeout — and what is new is the payload: which test, which seed, and whatever else the testbench needs told. All three travel as plusargs, which look like compiler defines, are not compiler defines, and are passed to a different command at a different step. The seed in particular has an edge that the previous module's advice walks straight into.

Intermediate18 min readPythonPart IIAutomationSimulationVerification Flow

Part II · Module 25 · Chapter 25.4 · Running One Test With a Seed and Plusargs

1. What a Plusarg Is

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
RUN_CMD = ["./simv", "+UVM_TESTNAME=axi_smoke", "+ntb_random_seed=1839274",
           "+UVM_VERBOSITY=UVM_LOW", "-l", "sim.log"]

print("the command:")
for part in RUN_CMD:
    kind = "plusarg" if part.startswith("+") else ("flag" if part.startswith("-") else "")
    print(f"   {part:<30} {kind}")

plusargs = [p for p in RUN_CMD if p.startswith("+")]
print()
print("plusargs:", len(plusargs))
print("they are read by:", "the testbench, at run time, via $value$plusargs")
print("not by:", "the simulator's own argument parser")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
the command:
   ./simv                         
   +UVM_TESTNAME=axi_smoke        plusarg
   +ntb_random_seed=1839274       plusarg
   +UVM_VERBOSITY=UVM_LOW         plusarg
   -l                             flag
   sim.log                        

plusargs: 3
they are read by: the testbench, at run time, via $value$plusargs
not by: the simulator's own argument parser

A plusarg is a value the testbench reads, not something the simulator acts on. The simulator's job is to pass it through; the SystemVerilog code calls $value$plusargs and gets the string back.

That distinction explains the most common symptom in this chapter: a misspelled plusarg produces no error at all. Nothing validates the name, because nothing knows what names are valid — the testbench asks for the ones it wants and silently gets its default for the ones it does not find.

2. The Two That Always Travel

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def run_command(binary, test, seed, extra=()):
    cmd = [binary]
    cmd += [f"+UVM_TESTNAME={test}", f"+ntb_random_seed={seed}"]
    cmd += list(extra)
    cmd += ["-l", "sim.log"]
    return cmd


print(run_command("./simv", "axi_smoke", 1839274))
print()
print(run_command("./simv", "axi_reset", 771002, ["+UVM_VERBOSITY=UVM_HIGH"]))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
['./simv', '+UVM_TESTNAME=axi_smoke', '+ntb_random_seed=1839274', '-l', 'sim.log']

['./simv', '+UVM_TESTNAME=axi_reset', '+ntb_random_seed=771002', '+UVM_VERBOSITY=UVM_HIGH', '-l', 'sim.log']

That is the canonical annotation:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cmd += [f"+UVM_TESTNAME={test}", f"+ntb_random_seed={seed}"]

+UVM_TESTNAME is how UVM's factory decides which test class to build, and +ntb_random_seed is VCS's spelling of the randomisation seed. Both are vendor- and framework-specific, which is why Chapter 23.11 Section 8 put them in an adapter: Questa wants -sv_seed, and the test name reaches the factory the same way but the seed does not.

The f"+NAME={value}" form is doing one quiet thing worth noticing — it calls str() on the seed. An int is fine here because the whole element is a string by the time it reaches the list, which is the difference between this and Chapter 24.1 Section 6's environment values, where a bare int raised.

3. +define+ Is Not a Plusarg

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
COMPILE = ["vcs", "-full64", "+define+DUMP_WAVES=1", "-f", "files.f", "-o", "simv"]
RUN = ["./simv", "+UVM_TESTNAME=axi_smoke", "+ntb_random_seed=1839274"]

print("compile step (Chapter 25.3):")
print("  ", " ".join(COMPILE))
print("run step (this chapter):")
print("  ", " ".join(RUN))
print()
print("both start with '+' :", True)
print("same step           :", False)
print("same mechanism      :", False)
print()
WRONG = ["./simv", "+define+DUMP_WAVES=1", "+UVM_TESTNAME=axi_smoke"]
print("a +define+ on the RUN command:")
print("  ", " ".join(WRONG))
print("   the simulator accepts it as an unrecognised plusarg")
print("   the testbench never asks for a plusarg called 'define+DUMP_WAVES'")
print("   symptom:", "the define had no effect, and nothing said so")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
compile step (Chapter 25.3):
   vcs -full64 +define+DUMP_WAVES=1 -f files.f -o simv
run step (this chapter):
   ./simv +UVM_TESTNAME=axi_smoke +ntb_random_seed=1839274

both start with '+' : True
same step           : False
same mechanism      : False

a +define+ on the RUN command:
   ./simv +define+DUMP_WAVES=1 +UVM_TESTNAME=axi_smoke
   the simulator accepts it as an unrecognised plusarg
   the testbench never asks for a plusarg called 'define+DUMP_WAVES'
   symptom: the define had no effect, and nothing said so

A +define+ is a compile-time macro and a plusarg is a run-time string, and they share a leading + for entirely historical reasons.

The failure is silent in both directions. A +define+ on the run command is swallowed as an unknown plusarg nobody asks for. A plusarg on the compile command is usually swallowed too. Neither produces a diagnostic, because neither tool has a list of valid names to check against.

The practical defence is structural: build the two commands in two different functions, which Chapters 25.3 and 25.4 do, so the question "compile or run?" is answered by which function you are editing rather than by remembering.

4. A Space in a Plusarg

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import shlex

value = "+UVM_TESTNAME=axi smoke"
cmd = ["./simv", value, "+ntb_random_seed=1"]

print("as a list, the tool receives:", len(cmd), "arguments")
print("the plusarg is intact       :", cmd[1])
print()
joined_naive = " ".join(cmd)
print("naively joined to a string  :", joined_naive)
print("read back by a shell        :", shlex.split(joined_naive))
print("arguments after the round trip:", len(shlex.split(joined_naive)))
print()
print("shlex.join keeps it whole   :", shlex.join(cmd))
print("round-trips                 :", shlex.split(shlex.join(cmd)) == cmd)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
as a list, the tool receives: 3 arguments
the plusarg is intact       : +UVM_TESTNAME=axi smoke

naively joined to a string  : ./simv +UVM_TESTNAME=axi smoke +ntb_random_seed=1
read back by a shell        : ['./simv', '+UVM_TESTNAME=axi', 'smoke', '+ntb_random_seed=1']
arguments after the round trip: 4

shlex.join keeps it whole   : ./simv '+UVM_TESTNAME=axi smoke' +ntb_random_seed=1
round-trips                 : True

Three arguments become four, and +UVM_TESTNAME arrives as axi with a stray smoke after it. Chapter 23.3 Section 2's word-splitting, met through a plusarg.

This is rarer with a test name and common with the values people actually put in plusargs — a comma-free description, a path on a shared drive with a space in it, a UVM config string. The list form from Chapter 23.2 makes it a non-issue, and Chapter 23.10's shlex.join is what keeps cmd.sh honest about it.

5. Seed 0 Is Falsy

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import random


def choose_seed():
    return random.Random().randrange(1, 2**31)


for seed in (0, 1, 1839274, None):
    coalesced = seed or "<rerolled>"
    explicit = "<rerolled>" if seed is None else seed
    print(f"seed={str(seed):<9} bool={str(bool(seed)):<6} "
          f"`seed or ...`={str(coalesced):<12} `is None`={explicit}")

print()
print("a generated seed is in range:", 1 <= choose_seed() < 2**31)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
seed=0         bool=False  `seed or ...`=<rerolled>   `is None`=0
seed=1         bool=True   `seed or ...`=1            `is None`=1
seed=1839274   bool=True   `seed or ...`=1839274      `is None`=1839274
seed=None      bool=False  `seed or ...`=<rerolled>   `is None`=<rerolled>

a generated seed is in range: True

seed = seed or choose_seed() silently rerolls seed 0.

Seed 0 is a perfectly ordinary seed, and it is the one somebody picks first when reproducing a bug by hand. Under the or form, asking for it gets you a different random seed, a different stimulus, and a test that passes — which reads as "I cannot reproduce it".

This matters more than an ordinary bug because of where it came from. Chapter 24.2 Section 2 argued for or over a default argument, and it was right there: an environment variable exported as the empty string should fall back, because an empty tool home is never what anybody meant. The rule does not generalise.

or tests truthiness; is None tests absence. Use or only when every falsy value of that type is genuinely a non-answer.

For a string path, "" is a non-answer. For a seed, 0 is an answer. For a count, 0 is an answer. For a boolean flag, False is emphatically an answer. The discriminator is not about Python — it is about whether the domain has a meaningful falsy value, and that is a question only you can answer about your own data.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import random


def choose_seed():
    return random.Random().randrange(1, 2**31)


def resolve_seed(seed=None):
    """None means 'pick one'. Anything else, including 0, is the caller's choice."""
    if seed is None:
        return choose_seed(), "generated"
    if not isinstance(seed, int) or isinstance(seed, bool):
        raise TypeError(f"seed must be an int, got {type(seed).__name__}")
    if seed < 0:
        raise ValueError(f"seed must not be negative, got {seed}")
    return seed, "given"


for candidate in (0, 1839274, None):
    value, origin = resolve_seed(candidate)
    shown = value if origin == "given" else "<generated>"
    print(f"asked for {str(candidate):<9} -> {origin:<10} {shown}")

for bad in (True, -1):
    try:
        resolve_seed(bad)
    except (TypeError, ValueError) as exc:
        print(f"asked for {str(bad):<9} -> {type(exc).__name__}: {exc}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
asked for 0         -> given      0
asked for 1839274   -> given      1839274
asked for None      -> generated  <generated>
asked for True      -> TypeError: seed must be an int, got bool
asked for -1        -> ValueError: seed must not be negative, got -1

Three guards, and the middle one is the non-obvious one.

isinstance(seed, bool) is rejected explicitly, because bool is a subclass of int in Python — so isinstance(True, int) is True and a stray waves=True landing in the seed position would sail through as seed 1. Chapter 25.1's keyword-only signature makes that unlikely; the guard makes it impossible.

The origin is returned alongside the value, so the record can say whether the seed was chosen by the caller or by the flow. "Random" in a regression means generated and then recorded, never generated again on each run — Chapter 20.8's point, and the reason a RunResult always carries the seed it actually used.

6. Plusargs From Configuration

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
PLUSARGS = {"UVM_VERBOSITY": "UVM_LOW", "UVM_MAX_QUIT_COUNT": 5,
            "UVM_TIMEOUT": "1000000,NO", "fsdb+all": None}


def plusarg_list(mapping):
    """Deterministic, and a None value means a bare flag."""
    out = []
    for name in sorted(mapping):
        value = mapping[name]
        out.append(f"+{name}" if value is None else f"+{name}={value}")
    return out


args = plusarg_list(PLUSARGS)
for a in args:
    print("  ", a)

print()
shuffled = dict(reversed(list(PLUSARGS.items())))
print("same dict, different insertion order, same command:",
      plusarg_list(shuffled) == args)
print("bare flag survived:", "+fsdb+all" in args)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   +UVM_MAX_QUIT_COUNT=5
   +UVM_TIMEOUT=1000000,NO
   +UVM_VERBOSITY=UVM_LOW
   +fsdb+all

same dict, different insertion order, same command: True
bare flag survived: True

sorted(mapping) makes the command a function of the configuration, not of the order somebody happened to write the keys — Chapter 25.2 Section 2's argument about the filelist, applied to the run command. Two runs from the same config file produce byte-identical commands, so Chapter 23.10's cmd.sh files are comparable and Chapter 24.6's "what changed?" has a sensible answer.

A None value means a bare flag, because +fsdb+all takes no value and a builder that forced = on everything would emit +fsdb+all=None.

Note that UVM_TIMEOUT's value contains a comma and needs no special handling — the list form carries it whole. Only the shell would have cared.

7. Duplicates, and Who Resolves Them

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
from collections import Counter

PAIRS = [("UVM_VERBOSITY", "UVM_LOW"),
         ("UVM_TESTNAME", "axi_smoke"),
         ("UVM_VERBOSITY", "UVM_HIGH")]

as_dict = dict(PAIRS)
as_list = [f"+{k}={v}" for k, v in PAIRS]

print("pairs supplied   :", len(PAIRS))
print("the dict holds   :", len(as_dict))
print("which value won  :", as_dict["UVM_VERBOSITY"])
print("which was lost   :", PAIRS[0][1])
print()
print("as a list        :", as_list)
print("duplicates kept  :", sum(1 for a in as_list if a.startswith("+UVM_VERBOSITY=")))
print()
print("duplicate keys   :", [k for k, n in Counter(k for k, _ in PAIRS).items() if n > 1])
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
pairs supplied   : 3
the dict holds   : 2
which value won  : UVM_HIGH
which was lost   : UVM_LOW

as a list        : ['+UVM_VERBOSITY=UVM_LOW', '+UVM_TESTNAME=axi_smoke', '+UVM_VERBOSITY=UVM_HIGH']
duplicates kept  : 2

duplicate keys   : ['UVM_VERBOSITY']

Three plusargs went in and the dict holds two. A dict resolves the collision silently, in favour of whichever came last — which is a policy decision nobody wrote down and nobody can see in the output.

The list form keeps both and hands the decision to the tool, which is worse rather than better: now the resolution depends on a simulator's argument-parsing order, it can differ between vendors, and Chapter 23.11's adapters would have to agree about it.

Neither silent resolution is right. The third line is:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
from collections import Counter


class PlusargError(Exception):
    """Two sources set the same plusarg to different values."""


def merge_plusargs(*sources):
    """Later sources override earlier ones — and a same-source clash is an error."""
    merged = {}
    for name, mapping in sources:
        clashes = [k for k in mapping if k in merged and merged[k][1] != mapping[k]]
        for key in clashes:
            prev_source, prev_value = merged[key]
            if prev_source == name:
                raise PlusargError(f"{key} set twice in {name}")
        for key, value in mapping.items():
            merged[key] = (name, value)
    return {k: v for k, (_, v) in merged.items()}, {k: s for k, (s, _) in merged.items()}


DEFAULTS = {"UVM_VERBOSITY": "UVM_LOW", "UVM_MAX_QUIT_COUNT": 5}
PER_TEST = {"UVM_VERBOSITY": "UVM_HIGH"}
CLI = {"UVM_MAX_QUIT_COUNT": 1}

values, origins = merge_plusargs(("defaults", DEFAULTS), ("per-test", PER_TEST), ("cli", CLI))
for key in sorted(values):
    print(f"{key:<20} {str(values[key]):<10} from {origins[key]}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
UVM_MAX_QUIT_COUNT   1          from cli
UVM_VERBOSITY        UVM_HIGH   from per-test

Every value carries where it came from, which is what turns "why is verbosity high?" from an investigation into a lookup. Chapter 21.6 layered configuration the same way and for the same reason.

The layering order is the policy, and reversing it is a real bug rather than a stylistic choice:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
DEFAULTS = {"UVM_VERBOSITY": "UVM_LOW", "UVM_MAX_QUIT_COUNT": 5}
PER_TEST = {"UVM_VERBOSITY": "UVM_HIGH"}
CLI = {"UVM_MAX_QUIT_COUNT": 1}

right = {**DEFAULTS, **PER_TEST, **CLI}
wrong = {**CLI, **PER_TEST, **DEFAULTS}

print("defaults, then per-test, then CLI:", dict(sorted(right.items())))
print("the same three, reversed         :", dict(sorted(wrong.items())))
print()
print("CLI override honoured  :", right["UVM_MAX_QUIT_COUNT"] == 1)
print("CLI override discarded :", wrong["UVM_MAX_QUIT_COUNT"] == 5)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
defaults, then per-test, then CLI: {'UVM_MAX_QUIT_COUNT': 1, 'UVM_VERBOSITY': 'UVM_HIGH'}
the same three, reversed         : {'UVM_MAX_QUIT_COUNT': 5, 'UVM_VERBOSITY': 'UVM_LOW'}

CLI override honoured  : True
CLI override discarded : True

Least specific first. {**a, **b} lets b win, so the most specific source goes last — defaults, then the per-test block, then whatever a person typed on the command line. Reversed, a user's explicit --max-quit-count 1 is silently replaced by the default, and the symptom is a flag that appears to do nothing.

8. The Run Step

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

plusargs = [a for a in sys.argv[1:] if a.startswith("+")]
test = next((a.split("=", 1)[1] for a in plusargs if a.startswith("+UVM_TESTNAME=")), "none")
seed = next((a.split("=", 1)[1] for a in plusargs if a.startswith("+ntb_random_seed=")), "none")

with open("sim.log", "w", encoding="utf-8") as log:
    log.write(f"UVM_INFO @ 0: reporter [RNTST] Running test {test}...\n")
    log.write(f"seed = {seed}\n")
    log.write(f"plusargs seen = {len(plusargs)}\n")
    if os.environ.get("FAKE_FAIL") == "1":
        log.write("UVM_ERROR drv.sv(88) @ 141900: response mismatch\n")
    log.write("TEST DONE\n")
sys.exit(1 if os.environ.get("FAKE_FAIL") == "1" else 0)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import os
import shlex
import subprocess
import sys
from pathlib import Path

SIMV = Path("fake_simv.py").resolve()


def plusarg_list(mapping):
    return [f"+{n}" if mapping[n] is None else f"+{n}={mapping[n]}" for n in sorted(mapping)]


def run_simulation(run_dir, test, seed, *, plusargs=None, fail=False, timeout_s=3600):
    run_dir = Path(run_dir)
    run_dir.mkdir(parents=True, exist_ok=True)

    cmd = [sys.executable, str(SIMV),
           f"+UVM_TESTNAME={test}", f"+ntb_random_seed={seed}"]
    cmd += plusarg_list(plusargs or {})

    (run_dir / "run_cmd.sh").write_text(
        "#!/bin/sh\nexec " + shlex.join(cmd) + "\n", encoding="utf-8")

    env = {**os.environ, "FAKE_FAIL": "1" if fail else "0"}
    cp = subprocess.run(cmd, cwd=run_dir, env=env, timeout=timeout_s,
                        stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
    return cp.returncode, run_dir / "sim.log"


rc, log = run_simulation("runs/axi_smoke_1839274", "axi_smoke", 1839274,
                         plusargs={"UVM_VERBOSITY": "UVM_LOW", "fsdb+all": None})

print("returncode:", rc)
print("the log the testbench wrote:")
for line in log.read_text(encoding="utf-8").splitlines():
    print("   " + line)

print()
rc, log = run_simulation("runs/axi_reset_771002", "axi_reset", 771002, fail=True)
print("failing run returncode:", rc)
print("errors in its log     :",
      sum(1 for l in log.read_text(encoding="utf-8").splitlines() if "UVM_ERROR" in l))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
returncode: 0
the log the testbench wrote:
   UVM_INFO @ 0: reporter [RNTST] Running test axi_smoke...
   seed = 1839274
   plusargs seen = 4
   TEST DONE

failing run returncode: 1
errors in its log     : 1

plusargs seen = 4 is the line worth checking: two from the annotation plus the two configured ones, all arriving intact at the other side of a process boundary.

That echo is a real technique and not just a demonstration. A testbench that logs the plusargs it actually received turns Section 1's silent-misspelling problem into a visible one — the flow said +UVM_VERBOSTIY=UVM_HIGH, the log says the testbench saw four plusargs and used its default verbosity, and the typo is findable. Without it, a misspelled plusarg is invisible from both sides.

Everything else is Module 23 unchanged: the list form, cwd=run_dir, the log written by the tool into the run directory, a timeout, and run_cmd.sh written before the run for Chapter 25.3 Section 6's reason.

9. Predict, Run, Observe

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import shlex

# A
print("A:", f"+ntb_random_seed={1839274}")

# B
print("B:", len(["./simv", "+A=1 2", "+B=3"]))

# C
print("C:", shlex.split(" ".join(["./simv", "+A=1 2", "+B=3"])))

# D
print("D:", shlex.join(["./simv", "+A=1 2"]))

# E
for s in (0, None):
    print("E:", s, "->", "rerolled" if not s else s, "|", "kept" if s is not None else "rerolled")

# F
print("F:", isinstance(True, int), isinstance(True, bool))

# G
m = {"b": 2, "a": None}
print("G:", [f"+{k}" if m[k] is None else f"+{k}={m[k]}" for k in sorted(m)])

# H
print("H:", "+define+X=1".startswith("+"), "a plusarg?", False)

The verified output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A: +ntb_random_seed=1839274
B: 3
C: ['./simv', '+A=1', '2', '+B=3']
D: ./simv '+A=1 2'
E: 0 -> rerolled | kept
E: None -> rerolled | rerolled
F: True True
G: ['+a', '+b=2']
H: True a plusarg? False

A — the f-string stringifies the seed, so an int is fine here.

B — three arguments in the list, whatever is inside them.

C — joined and re-split, four tokens and a broken plusarg.

D — shlex.join quotes exactly the one that needs it.

E — seed 0 is rerolled by or and kept by is None. The whole of Section 5.

F — True is an int, which is why the seed guard checks bool explicitly.

G — a None value becomes a bare flag, and sorted fixes the order.

H — a +define+ looks exactly like a plusarg and is not one.

10. Exercises

Exercise 1 — who reads it. Explain in two sentences what reads a plusarg and what does not.

Exercise 2 — the silent typo. Misspell a plusarg in a real flow and record what the tool says.

Exercise 3 — the two that always travel. Write the run command builder and show it for two different tests.

Exercise 4 — the other vendors. Find how your simulators spell the seed. Say where that belongs, citing Chapter 23.11.

Exercise 5 — two commands, two functions. Say what structural defence that gives against the define/plusarg confusion.

Exercise 6 — the swallowed define. Put a +define+ on a run command and describe the symptom.

Exercise 7 — the space. Build a plusarg with a space, join it naively and re-split. Report the token count.

Exercise 8 — realistic values. Name three plusarg values in your flow that could contain a space.

Exercise 9 — seed 0. Show seed or choose_seed() rerolling it, and say what a person reproducing a bug by hand concludes.

Exercise 10 — the counterpoint. Reconcile this with Chapter 24.2's advice. State the discriminator in one sentence.

Exercise 11 — your own falsy values. List three fields in your flow where 0, "" or False is a real answer.

Exercise 12 — the bool guard. Show isinstance(True, int) and say what it would let through.

Exercise 13 — origin. Return whether the seed was given or generated. Say what the record does with that.

Exercise 14 — random means recorded. Explain in two sentences why a regression's "random" seed must be generated once.

Exercise 15 — deterministic plusargs. Build them from a dict and prove the order does not depend on insertion.

Exercise 16 — the bare flag. Handle a None value and say what a naive builder would emit.

Exercise 17 — echo them. Make a stand-in testbench log the plusargs it received, and say what that makes findable.

Exercise 18 — the silent drop. Build a plusarg mapping from pairs containing a duplicate key. Report how many survive and which.

Exercise 19 — neither default. Say why letting the tool resolve a duplicate is worse than letting a dict resolve it.

Exercise 20 — carry the origin. Merge three layers and print where each final value came from. Say what question that answers instantly.

Exercise 21 — reverse the merge. Show a CLI override being discarded, and describe what the user concludes about the flag.

Exercise 22 — before the run. Say why run_cmd.sh is written before the simulator is launched.

11. Summary

  • A plusarg is read by the testbench at run time, via $value$plusargs, not by the simulator's own parser.
  • Nothing validates a plusarg name, so a misspelling produces no error — the testbench quietly uses its default.
  • +UVM_TESTNAME and +ntb_random_seed always travel, and both spellings are vendor- and framework-specific, so they belong in Chapter 23.11's adapter.
  • +define+ is compile-time and a plusarg is run-time. They share a + and nothing else.
  • Both misplacements are silent, so build the two commands in two different functions and let the structure answer the question.
  • A plusarg value with a space survives the list form and is destroyed by a naive join — three arguments become four.
  • Seed 0 is falsy, so seed = seed or choose_seed() silently rerolls the seed somebody picked to reproduce a bug.
  • or tests truthiness; is None tests absence. Chapter 24.2's advice was right for a string path and is wrong here.
  • The discriminator is whether the domain has a meaningful falsy value — 0 for a seed or a count, False for a flag.
  • bool is a subclass of int, so a seed guard must reject True explicitly.
  • Return whether the seed was given or generated, and record the seed that was actually used.
  • "Random" means generated once and recorded, never generated again on each run.
  • Build plusargs from a sorted mapping, so the command is a function of the configuration rather than of insertion order.
  • A None value means a bare flag, or the builder emits +fsdb+all=None.
  • Have the testbench echo the plusargs it received, which is what makes a misspelling findable from the log.
  • A dict silently resolves a duplicate plusarg in favour of the last one, which is a policy nobody wrote down.
  • A list keeps both and hands the decision to the tool, which is worse — the answer then varies by vendor.
  • Raise on a same-source clash and layer deliberately across sources, carrying the origin of every value.
  • Least specific first: defaults, then per-test, then the command line, because {**a, **b} lets b win.
  • Reversing the merge discards the user's explicit flag, and a flag that appears to do nothing is reported once and then abandoned.
  • Write run_cmd.sh before launching, so the timeout path still produces it.

The next chapter adds the one plusarg that is a policy decision rather than a parameter: whether to dump a waveform.

Where this fits

Part of the Python curriculum.