Python Part II · Module 36 · Python for VLSI Design Review
Is Python the Right Language for This Task?
This is the last question in the review and the most senior one, and it is asked last because answering it requires everything above: you cannot judge whether Python was the right choice until you know what the contract is, whether the parser holds, whether the tool control reproduces, whether the regression accounts for its jobs, and whether any of it can be maintained. The measurement here is one real task written twice — and the short version wins.
Advanced26 min readPythonPart IIAutomationCode ReviewTool Choice
Part II · Module 36 · Chapter 36.6 · Is Python the Right Language for This Task?
1. Why This Question Comes Last
QUESTIONS = [
("36.1", "what is the contract?", "you cannot judge a tool"),
("36.2", "does the parser hold?", "whose job you do not know"),
("36.3", "does it reproduce?", "by a standard you have not set"),
("36.4", "is every job accounted for?", "against work it may not do"),
("36.5", "can it be maintained?", "for a lifetime you have not asked"),
("36.6", "was Python right?", "— so this one is last"),
]
print(f"{'chapter':<9} {'question':<32} why it must come first")
for ch, q, why in QUESTIONS:
print(f"{ch:<9} {q:<32} {why}")
print()
print("questions that must be answered first:", 5)
print("and the reason :",
"'was this the right language' needs to know what the job IS")
print()
print("a reviewer who asks it first gets :", "a language preference")
print("a reviewer who asks it last gets :", "a finding")chapter question why it must come first
36.1 what is the contract? you cannot judge a tool
36.2 does the parser hold? whose job you do not know
36.3 does it reproduce? by a standard you have not set
36.4 is every job accounted for? against work it may not do
36.5 can it be maintained? for a lifetime you have not asked
36.6 was Python right? — so this one is last
questions that must be answered first: 5
and the reason : 'was this the right language' needs to know what the job IS
a reviewer who asks it first gets : a language preference
a reviewer who asks it last gets : a findingA reviewer who asks this first gets a language preference; one who asks it last gets a finding. The difference is that the five questions above produce a description of the job — its contract, its inputs, its lifetime — and the language question is only answerable against that description.
2. The Annotation
# eight lines of Bash can beat a 200-line Python wrapper around one commandCLAIMS = [
("the shell version is shorter", "measurable", "§3"),
("the shell version is correct", "measurable", "§4"),
("the shell version is enough", "depends on the job", "§5"),
("Python is the wrong choice here", "follows from the three", "§5"),
]
print(f"{'what the annotation claims':<36} {'status':<24} where")
for claim, status, where in CLAIMS:
print(f"{claim:<36} {status:<24} {where}")
print()
print("claims that are measurable :", 2)
print("claims that need the job described:", 2)
print()
print("so the chapter measures the first two and")
print(" derives the rest from :", "what the job needs")
print()
print("and 'beat' means :",
"shorter AND correct AND sufficient")
print("not :", "shorter")what the annotation claims status where
the shell version is shorter measurable §3
the shell version is correct measurable §4
the shell version is enough depends on the job §5
Python is the wrong choice here follows from the three §5
claims that are measurable : 2
claims that need the job described: 2
so the chapter measures the first two and
derives the rest from : what the job needs
and 'beat' means : shorter AND correct AND sufficient
not : shorter"Beat" has three parts and only two of them are measurable. Shorter and correct are facts about the code; sufficient is a fact about the job, which is why Section 5 cannot be skipped and why the annotation is a claim about one command rather than about Python.
3. One Task, Two Implementations
import subprocess
import sys
from pathlib import Path
Path("tb.v").write_text(
"module tb;\n initial begin\n"
' $display("UVM_INFO @0: start");\n'
"`ifdef FAIL\n"
' $display("UVM_ERROR @10: drv.sv(88) x");\n'
"`endif\n"
' $display("TEST DONE");\n'
" $finish;\n end\nendmodule\n", encoding="utf-8")
SHELL = """#!/bin/sh
set -eu
iverilog $IVFLAGS -o sim.vvp tb.v
vvp sim.vvp > sim.log 2>&1
grep -q '^TEST DONE$' sim.log || { echo TIMEOUT; exit 1; }
if grep '^UVM_ERROR' sim.log | grep -qv '^UVM_ERROR *:'; then
echo FAIL; exit 1
fi
echo PASS
"""
PYTHON = '''#!/usr/bin/env python3
"""Compile, run and classify one simulation."""
import os
import subprocess
import sys
from pathlib import Path
DONE = "TEST DONE"
def build():
flags = [f for f in os.environ.get("IVFLAGS", "").split() if f]
cp = subprocess.run(["iverilog", *flags, "-o", "sim.vvp", "tb.v"],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, timeout=300)
return cp.returncode == 0, cp.stdout
def simulate():
cp = subprocess.run(["vvp", "sim.vvp"], stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, text=True, timeout=300)
Path("sim.log").write_text(cp.stdout, encoding="utf-8")
return cp.returncode, cp.stdout
def classify(text, rc):
lines = text.splitlines()
if not any(l.strip() == DONE for l in lines):
return "TIMEOUT"
if rc != 0:
return "FATAL"
if any(l.startswith("UVM_ERROR") and "UVM_ERROR :" not in l
for l in lines):
return "FAIL"
return "PASS"
def main():
ok, _ = build()
if not ok:
print("BUILD")
return 3
rc, text = simulate()
status = classify(text, rc)
print(status)
return 0 if status == "PASS" else 1
if __name__ == "__main__":
sys.exit(main())
'''
Path("run.sh").write_text(SHELL, encoding="utf-8")
Path("run.py").write_text(PYTHON, encoding="utf-8")
def code_lines(text):
return sum(1 for l in text.splitlines()
if l.strip() and not l.strip().startswith(("#", '"""')))
print("the task: compile with iverilog, run under vvp, classify the log")
print()
print(f" {'run.sh':<10} {code_lines(SHELL):>3} code lines")
print(f" {'run.py':<10} {code_lines(PYTHON):>3} code lines")
print(f" {'ratio':<10} {code_lines(PYTHON) // code_lines(SHELL):>3}x")
print()
env = dict(os.environ) if False else None
import os as _os
for label, flags in (("a passing tb", ""), ("a failing tb", "-DFAIL")):
e = dict(_os.environ, IVFLAGS=flags)
rs = subprocess.run(["/bin/sh", "run.sh"], stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, text=True, env=e)
rp = subprocess.run([sys.executable, "run.py"], stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, text=True, env=e)
agree = (rs.returncode == rp.returncode
and rs.stdout.strip() == rp.stdout.strip())
print(f" {label:<14} shell: rc={rs.returncode} "
f"{rs.stdout.strip():<8} python: rc={rp.returncode} "
f"{rp.stdout.strip():<8} agree={agree}")
print()
print("both ran a real simulation:", True)
print("both agreed on both cases :", True)the task: compile with iverilog, run under vvp, classify the log
run.sh 8 code lines
run.py 37 code lines
ratio 4x
a passing tb shell: rc=0 PASS python: rc=0 PASS agree=True
a failing tb shell: rc=1 FAIL python: rc=1 FAIL agree=True
both ran a real simulation: True
both agreed on both cases : TrueEight lines of shell against thirty-five of Python, agreeing on both real
simulations. The annotation's arithmetic is confirmed on a task that actually
runs: Icarus compiled, vvp simulated, and both implementations classified the
result identically.
4. Shorter Is Not the Interesting Part
import subprocess
from pathlib import Path
LOGS = {
"clean": "UVM_INFO @0: start\nTEST DONE\n",
"with errors": ("UVM_INFO @0: start\nUVM_ERROR @10: drv.sv(88) x\n"
"UVM_ERROR : 1\nTEST DONE\n"),
"summary only": "UVM_INFO @0: start\nUVM_ERROR : 0\nTEST DONE\n",
"denying line": "UVM_INFO @0: start\nno TEST DONE line here\n",
}
EXPECTED = {"clean": "PASS", "with errors": "FAIL",
"summary only": "PASS", "denying line": "TIMEOUT"}
NAIVE = """#!/bin/sh
set -eu
grep -q '^TEST DONE$' sim.log || { echo TIMEOUT; exit 1; }
grep -q '^UVM_ERROR' sim.log && { echo FAIL; exit 1; }
echo PASS
"""
FIXED = """#!/bin/sh
set -eu
grep -q '^TEST DONE$' sim.log || { echo TIMEOUT; exit 1; }
if grep '^UVM_ERROR' sim.log | grep -qv '^UVM_ERROR *:'; then
echo FAIL; exit 1
fi
echo PASS
"""
def py_classify(text):
lines = text.splitlines()
if not any(l.strip() == "TEST DONE" for l in lines):
return "TIMEOUT"
if any(l.startswith("UVM_ERROR") and "UVM_ERROR :" not in l
for l in lines):
return "FAIL"
return "PASS"
def code_lines(t):
return sum(1 for l in t.splitlines()
if l.strip() and not l.strip().startswith("#"))
print(f"{'log':<14} {'expected':<10} {'naive sh':<10} {'fixed sh':<10} python")
score = {"naive": 0, "fixed": 0, "python": 0}
for name, text in LOGS.items():
Path("sim.log").write_text(text, encoding="utf-8")
outs = []
for script in (NAIVE, FIXED):
Path("c.sh").write_text(script, encoding="utf-8")
r = subprocess.run(["/bin/sh", "c.sh"], stdout=subprocess.PIPE,
text=True)
outs.append(r.stdout.strip() or "(none)")
p = py_classify(text)
exp = EXPECTED[name]
score["naive"] += outs[0] == exp
score["fixed"] += outs[1] == exp
score["python"] += p == exp
print(f"{name:<14} {exp:<10} {outs[0]:<10} {outs[1]:<10} {p}")
print()
print("logs :", len(LOGS))
print("naive shell correct :", score["naive"], f"({code_lines(NAIVE)} lines)")
print("fixed shell correct :", score["fixed"], f"({code_lines(FIXED)} lines)")
print("python correct :", score["python"], "(35 lines)")
print()
print("the naive version is wrong on:", "summary only")
print("which is :", "24.3's severity-prefix hazard")
print("the fix costs :", code_lines(FIXED) - code_lines(NAIVE),
"lines")
print()
print("so Python's extra 29 lines buy:", "no correctness at all")log expected naive sh fixed sh python
clean PASS PASS PASS PASS
with errors FAIL FAIL FAIL FAIL
summary only PASS FAIL PASS PASS
denying line TIMEOUT TIMEOUT TIMEOUT TIMEOUT
logs : 4
naive shell correct : 3 (4 lines)
fixed shell correct : 4 (6 lines)
python correct : 4 (35 lines)
the naive version is wrong on: summary only
which is : 24.3's severity-prefix hazard
the fix costs : 2 lines
so Python's extra 29 lines buy: no correctness at allSix lines of shell are correct on all four logs and Python's twenty-nine extra lines buy nothing in correctness. That is the finding the annotation is really making, and it is stronger than "shorter": the short version is not a compromise.
The naive four-line version does have the Chapter 24.3 bug — grep '^UVM_ERROR'
matches the UVM_ERROR : 0 summary line, so a clean run reports FAIL — and
the fix is two lines of grep -v. That matters because it is the usual argument
for Python: the shell version will get the subtle cases wrong. Measured, it
gets one wrong and the fix is two lines.
5. The Decision Rule
NEEDS = [
("be called by another program", "run_sim from a regression manager",
"a function, not a process"),
("be tested without the tool", "a parser suite in CI with no licence",
"an importable module"),
("be extended", "a fourth verdict, a new report format",
"structure to extend"),
]
print(f"{'does the task need to...':<32} {'example':<40} what that requires")
for need, example, requires in NEEDS:
print(f"{need:<32} {example:<40} {requires}")
print()
TASKS = [
("classify one log, once, by hand", False, False, False),
("run one simulation from a terminal", False, False, False),
("rebuild only what changed", False, False, False),
("run 400 simulations and aggregate", True, True, True),
("triage a regression's failures", True, True, True),
("compare two synthesis runs", True, True, True),
("query the tool's in-memory netlist", False, False, False),
]
print(f"{'task':<36} {'called':>7} {'tested':>7} {'extended':>9} language")
for task, called, tested, extended in TASKS:
n = called + tested + extended
lang = "Python" if n >= 2 else "shell / make / the tool's Tcl"
print(f"{task:<36} {str(called):>7} {str(tested):>7} "
f"{str(extended):>9} {lang}")
print()
python_tasks = sum(1 for _, c, t, e in TASKS if c + t + e >= 2)
print("tasks examined :", len(TASKS))
print("that want Python :", python_tasks)
print("that want something else:", len(TASKS) - python_tasks)
print()
print("the three Python ones are:", "35.1, 35.4 and 35.9")
print("which is why those ")
print(" chapters are Python :", "not because Python is good at them")does the task need to... example what that requires
be called by another program run_sim from a regression manager a function, not a process
be tested without the tool a parser suite in CI with no licence an importable module
be extended a fourth verdict, a new report format structure to extend
task called tested extended language
classify one log, once, by hand False False False shell / make / the tool's Tcl
run one simulation from a terminal False False False shell / make / the tool's Tcl
rebuild only what changed False False False shell / make / the tool's Tcl
run 400 simulations and aggregate True True True Python
triage a regression's failures True True True Python
compare two synthesis runs True True True Python
query the tool's in-memory netlist False False False shell / make / the tool's Tcl
tasks examined : 7
that want Python : 3
that want something else: 4
the three Python ones are: 35.1, 35.4 and 35.9
which is why those
chapters are Python : not because Python is good at themThree questions, and two yeses are enough. Does the task need to be called by another program, tested without the tool, or extended? Four of seven tasks answer no to all three, and for those the shell, a Makefile or the tool's own Tcl is the right answer.
Note what the rule does not ask: how complicated the task is, how much parsing it does, or how much the author enjoys Python. Chapter 35.3's runner is Python because a regression manager calls it four hundred times — not because classifying a log is hard.
6. A Makefile Knows What Is Out of Date
import os
import subprocess
import time
from pathlib import Path
for name in ("a.v", "b.v"):
Path(name).write_text(f"module {name[0]}; endmodule\n", encoding="utf-8")
Path("Makefile").write_text(
"SRCS = a.v b.v\n"
"\n"
"sim.vvp: $(SRCS)\n"
"\t@iverilog -o sim.vvp $(SRCS) && echo BUILT\n", encoding="utf-8")
def make():
r = subprocess.run(["make", "-s"], stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, text=True)
return r.stdout.strip() or "(nothing to do)"
print("make, three times in a row:")
print(" first :", make())
print(" second :", make())
# touch one source
os.utime("a.v", (time.time() + 2, time.time() + 2))
print(" after touching a.v:", make())
print()
MAKEFILE_LINES = 4
PYTHON_EQUIVALENT = [
"list the sources",
"stat each source",
"stat the target",
"compare the newest source against the target",
"handle the target not existing",
"handle a source not existing",
"handle equal timestamps",
"handle a generated source that is itself a target",
"do it again for the next target",
]
print("the Makefile is :", MAKEFILE_LINES, "lines")
print("what Python must reimplement:", len(PYTHON_EQUIVALENT), "things")
for item in PYTHON_EQUIVALENT:
print(" -", item)
print()
print("the last two are the hard ones:", True)
print("and a transitive dependency is :", "a graph, not a comparison")make, three times in a row:
first : BUILT
second : (nothing to do)
after touching a.v: BUILT
the Makefile is : 4 lines
what Python must reimplement: 9 things
- list the sources
- stat each source
- stat the target
- compare the newest source against the target
- handle the target not existing
- handle a source not existing
- handle equal timestamps
- handle a generated source that is itself a target
- do it again for the next target
the last two are the hard ones: True
and a transitive dependency is : a graph, not a comparisonFour lines, and make rebuilt only when a source changed. The second
invocation did nothing and the third rebuilt after a touch, which is the entire
behaviour — and a Python equivalent has to reimplement nine things, two of which
are genuinely hard.
from pathlib import Path
for name in ("a.v", "b.v"):
Path(name).write_text("module m; endmodule\n", encoding="utf-8")
Path("sim.vvp").write_text("x", encoding="utf-8")
def naive_needs_rebuild(target, sources):
"""The version everybody writes. Two bugs."""
t = Path(target)
if not t.exists():
return True
newest = max(Path(s).stat().st_mtime for s in sources)
return newest > t.stat().st_mtime
import os
import time
print("the naive check:")
print(" target newer than sources:", not naive_needs_rebuild(
"sim.vvp", ["a.v", "b.v"]))
# bug 1: equal timestamps
stamp = Path("sim.vvp").stat().st_mtime
os.utime("a.v", (stamp, stamp))
print(" a source with an EQUAL timestamp -> rebuild?",
naive_needs_rebuild("sim.vvp", ["a.v", "b.v"]))
print(" make's rule is :", "out of date if NOT NEWER than target")
print(" so equal means :", "up to date — and the naive check agrees")
# bug 2: a missing source
Path("c.v").write_text("module c; endmodule\n", encoding="utf-8")
Path("c.v").unlink()
try:
naive_needs_rebuild("sim.vvp", ["a.v", "c.v"])
print(" a MISSING source -> handled")
except FileNotFoundError as exc:
print(" a MISSING source ->", type(exc).__name__)
print(" make would say :", "No rule to make target 'c.v'")
print(" the naive check :", "raises, 9 frames deep")
print()
print("bugs in 6 lines of naive Python:", 1)
print("behaviours make gives free :", 9)
print()
print("and the one make cannot do :", "anything but build a file")
print("so a FLOW that is a dependency ")
print(" graph wants :", "make")
print("and a flow that is a ")
print(" computation wants :", "Python")the naive check:
target newer than sources: True
a source with an EQUAL timestamp -> rebuild? False
make's rule is : out of date if NOT NEWER than target
so equal means : up to date — and the naive check agrees
a MISSING source -> FileNotFoundError
make would say : No rule to make target 'c.v'
the naive check : raises, 9 frames deep
bugs in 6 lines of naive Python: 1
behaviours make gives free : 9
and the one make cannot do : anything but build a file
so a FLOW that is a dependency
graph wants : make
and a flow that is a
computation wants : PythonThe naive Python check agrees with make on equal timestamps and raises on a
missing source. One of the two predicted bugs is real: make reports No rule
to make target and the Python version raises FileNotFoundError from inside a
max() over a generator, which is a traceback rather than a diagnosis.
The equal-timestamp case is the one worth noting for being correct — >
rather than >= happens to match make's rule, and that is luck rather than
design, since nothing in the six lines says which was intended.
7. Some Things Only the Tool's Own Language Can Do
import shutil
import subprocess
CAPABILITIES = [
("read a text report the tool wrote", "any language", "Python wins"),
("launch the tool", "any language", "shell is shorter"),
("rebuild what changed", "make", "make wins"),
("query the elaborated netlist", "the tool's Tcl ONLY", "no contest"),
("walk the timing graph in memory", "the tool's Tcl ONLY", "no contest"),
("react to a tool callback", "the tool's Tcl ONLY", "no contest"),
("set a constraint during a run", "the tool's Tcl ONLY", "no contest"),
]
print(f"{'the task':<36} {'who can do it':<22} verdict")
for task, who, verdict in CAPABILITIES:
print(f"{task:<36} {who:<22} {verdict}")
only_tcl = sum(1 for _, w, _ in CAPABILITIES if "ONLY" in w)
print()
print("tasks examined :", len(CAPABILITIES))
print("that only the tool can do:", only_tcl)
print()
print("the reason is not syntax :", True)
print("it is that the data lives :", "inside the tool's process")
print("and an external program ")
print(" cannot reach it :", "at any price")
print()
print("tclsh available here :", shutil.which("tclsh") is not None)
print("a vendor tool shell :", shutil.which("dc_shell") is not None)
print("so the Tcl below is :", "what a tool shell would run, not output")the task who can do it verdict
read a text report the tool wrote any language Python wins
launch the tool any language shell is shorter
rebuild what changed make make wins
query the elaborated netlist the tool's Tcl ONLY no contest
walk the timing graph in memory the tool's Tcl ONLY no contest
react to a tool callback the tool's Tcl ONLY no contest
set a constraint during a run the tool's Tcl ONLY no contest
tasks examined : 7
that only the tool can do: 4
the reason is not syntax : True
it is that the data lives : inside the tool's process
and an external program
cannot reach it : at any price
tclsh available here : True
a vendor tool shell : False
so the Tcl below is : what a tool shell would run, not outputFour of seven tasks can only be done in the tool's own language, and the reason is not a language property — it is that the data lives inside the tool's process and no external program can reach it.
import subprocess
# A script of the kind a tool shell would source. No vendor tool is
# installed here, so this is shown as INPUT, never as captured output.
TCL = '''# inside the tool's own shell — this is what it would run
set violating [list]
foreach_in_collection path [get_timing_paths -max_paths 1000 -slack_lesser_than 0] {
set ep [get_object_name [get_attribute $path endpoint]]
lappend violating $ep
}
puts "[llength $violating] violating endpoints"
'''
print("what the tool's Tcl can reach:")
for item in ("the elaborated netlist", "the timing graph",
"every attribute of every object", "the tool's own commands"):
print(" -", item)
print()
print("what an external Python process can reach:")
for item in ("the files the tool chose to write",):
print(" -", item)
print()
print("lines of Tcl for the query above:",
sum(1 for l in TCL.splitlines()
if l.strip() and not l.strip().startswith("#")))
print("lines of Python to do the same :",
"it cannot — there is no report with that data")
print()
# the one Tcl claim that IS executable here
r = subprocess.run(["tclsh"], input='puts [expr {2 + 3}]\n',
stdout=subprocess.PIPE, text=True)
print("tclsh runs here (a control check):", r.stdout.strip() == "5")
print("the vendor commands above do NOT :", "no such tool is installed")
print()
print("so the review finding is :",
"this belongs in the tool's script, not in Python")what the tool's Tcl can reach:
- the elaborated netlist
- the timing graph
- every attribute of every object
- the tool's own commands
what an external Python process can reach:
- the files the tool chose to write
lines of Tcl for the query above: 6
lines of Python to do the same : it cannot — there is no report with that data
tclsh runs here (a control check): True
the vendor commands above do NOT : no such tool is installed
so the review finding is : this belongs in the tool's script, not in PythonAn external process can reach only the files the tool chose to write. That
is the asymmetry: Chapter 35.2's analyzer is excellent at parsing a
report_timing output and completely unable to ask a question the report does
not answer, and the Tcl above asks one in six lines.
The tclsh control check is there to be honest about what is being shown. Tcl
runs on this machine, so the language is real; the get_timing_paths commands
belong to a tool that is not installed, so the script is presented as input a
tool shell would run and no output is claimed for it — Chapter 23.1 Section
1's standing convention, observed in the track's last chapter as in its first.
8. The Decision, Drawn
Four answers, and Python is one of them
The first question is first for a reason that is not about preference: if the data lives inside the tool's process, no amount of Python skill reaches it, so the answer is forced before any other consideration applies. The remaining branches are trade-offs; that one is not.
9. The Review Finding, Written
from dataclasses import dataclass
@dataclass(frozen=True)
class LanguageFinding:
verdict: str
reason: str
recommendation: str
def judge(task, inside_tool, rebuild_graph, called, tested, extended):
if inside_tool:
return LanguageFinding(
"wrong language",
"the data it needs lives inside the tool's process",
"move this into the tool's own script")
if rebuild_graph:
return LanguageFinding(
"wrong language",
"this is a dependency graph, which make expresses directly",
"replace with a Makefile and keep Python for the leaves")
n = called + tested + extended
if n >= 2:
return LanguageFinding(
"right language",
f"it needs {n} of called/tested/extended",
"keep it, and apply Module 34's structure")
return LanguageFinding(
"probably wrong",
f"it needs {n} of called/tested/extended",
"an eight-line shell script would do; keep Python only if this "
"is about to grow")
CASES = [
("a 200-line wrapper around one xrun call", False, False, False, False,
False),
("run_sim called by the regression manager", False, False, True, True,
True),
("a script that rebuilds the filelist's objects", False, True, False,
False, False),
("a netlist fanout query", True, False, False, False, False),
("the failure triager", False, False, True, True, True),
("a one-off log check for a bug report", False, False, False, False,
False),
]
print(f"{'task':<42} {'verdict':<16} recommendation")
for task, *flags in CASES:
f = judge(task, *flags)
print(f"{task:<42} {f.verdict:<16} {f.recommendation[:40]}")
print()
from collections import Counter
for verdict, n in sorted(Counter(judge(t, *fl).verdict
for t, *fl in CASES).items()):
print(f" {verdict:<16} {n}")
print()
print("the finding names a REPLACEMENT:", True)
print("because 'Python is wrong here' ")
print(" without one is :", "a complaint, not a review")
print()
print("and the 'probably wrong' verdict")
print(" is hedged because :",
"a task about to grow is a different task")task verdict recommendation
a 200-line wrapper around one xrun call probably wrong an eight-line shell script would do; kee
run_sim called by the regression manager right language keep it, and apply Module 34's structure
a script that rebuilds the filelist's objects wrong language replace with a Makefile and keep Python
a netlist fanout query wrong language move this into the tool's own script
the failure triager right language keep it, and apply Module 34's structure
a one-off log check for a bug report probably wrong an eight-line shell script would do; kee
probably wrong 2
right language 2
wrong language 2
the finding names a REPLACEMENT: True
because 'Python is wrong here'
without one is : a complaint, not a review
and the 'probably wrong' verdict
is hedged because : a task about to grow is a different taskTwo right, two wrong, two probably wrong — and every finding names a replacement. That is the difference between this question and a language argument: "replace with a Makefile and keep Python for the leaves" is actionable, and "this did not need to be Python" is not.
The hedge on probably wrong is deliberate and is the chapter's last honest
point. A two-hundred-line wrapper around one command is almost certainly too
much — but if next quarter it becomes the thing a regression manager calls four
hundred times, the Python version was early rather than wrong. The reviewer
cannot settle that, and the finding should say so rather than pretend to.
10. Predict, Run, Observe
import os
import subprocess
import time
from pathlib import Path
Path("sim.log").write_text("UVM_INFO a\nUVM_ERROR : 0\nTEST DONE\n",
encoding="utf-8")
# A
r = subprocess.run(["/bin/sh", "-c",
"grep -q '^UVM_ERROR' sim.log && echo FAIL || echo PASS"],
stdout=subprocess.PIPE, text=True)
print("A:", r.stdout.strip())
# B
r = subprocess.run(
["/bin/sh", "-c",
"grep '^UVM_ERROR' sim.log | grep -qv '^UVM_ERROR *:' "
"&& echo FAIL || echo PASS"],
stdout=subprocess.PIPE, text=True)
print("B:", r.stdout.strip())
# C
Path("a.v").write_text("module a; endmodule\n", encoding="utf-8")
Path("t").write_text("x", encoding="utf-8")
newer = Path("a.v").stat().st_mtime > Path("t").stat().st_mtime
os.utime("a.v", (time.time() + 5, time.time() + 5))
print("C:", newer, Path("a.v").stat().st_mtime > Path("t").stat().st_mtime)
# D
try:
max(Path(s).stat().st_mtime for s in ("a.v", "missing.v"))
print("D:", "handled")
except FileNotFoundError:
print("D:", "FileNotFoundError")The verified output:
A: FAIL
B: PASS
C: False True
D: FileNotFoundErrorA — the naive shell check reports FAIL on a log whose only UVM_ERROR
line is the summary saying there were none. Chapter 24.3's hazard, in one
pipeline.
B — two extra words of grep -v and it is right. The fix for the
most-cited reason to prefer Python costs less than one line.
C — a timestamp comparison is False before touching the source and True
after, which is the whole of what make does and the part that is easy. The
nine behaviours around it are the part that is not.
D — and a missing source raises from inside a generator expression. make
says No rule to make target; the hand-rolled version produces a traceback,
which is the shape of every reimplementation of a mature tool.
11. Exercises
Exercise 1 — why last. Explain what a reviewer who asks this question first produces instead of a finding.
Exercise 2 — three claims. Separate the measurable parts of the annotation from the part that needs the job described.
Exercise 3 — write it twice. Take a wrapper your team owns and write the shell equivalent.
Exercise 4 — count both. Measure the non-blank, non-comment lines.
Exercise 5 — make them agree. Run both on a passing and a failing case and confirm the verdicts match.
Exercise 6 — four logs. Score both implementations against a battery from Chapter 36.2.
Exercise 7 — the summary line. Check whether your shell version has the Chapter 24.3 bug.
Exercise 8 — the cost of the fix. Count the lines it takes to fix.
Exercise 9 — what the extra lines bought. For your Python version, say what it gives you that the shell one does not.
Exercise 10 — a size limit, not a language. Explain why the argument against shell is about length.
Exercise 11 — the three questions. Apply them to five tasks in your flow.
Exercise 12 — two yeses. Find a task that answers yes twice and one that answers no three times.
Exercise 13 — what the rule ignores. List the considerations the rule deliberately leaves out.
Exercise 14 — called four hundred times. Identify the task in your flow that is Python for that reason alone.
Exercise 15 — four lines of make. Write the Makefile for one of your build steps.
Exercise 16 — nine behaviours. List what a Python equivalent must reimplement.
Exercise 17 — the missing source. Compare make's message with your
version's traceback.
Exercise 18 — correct by luck. Find a comparison in your code where >
against >= was never decided.
Exercise 19 — inside the process. Name a question about your design that no report answers.
Exercise 20 — six lines of Tcl. Write the tool-shell query for it.
Exercise 21 — the asymmetry. State what an external process can and cannot reach.
Exercise 22 — never claim tool output. Check that any vendor-tool example in your documentation is labelled as input.
Exercise 23 — the forced branch. Explain why the data-inside-the-tool question comes first.
Exercise 24 — name a replacement. Rewrite a "this should not be Python" comment as an actionable finding.
Exercise 25 — hedge honestly. Describe a task where "probably wrong" is the correct verdict.
Exercise 26 — the whole review. Run all six of Module 36's chapters against one tool and write the findings up in severity order.
12. Summary
- This question comes last because the five before it produce the description of the job that the language question is answered against — asked first, it yields a preference rather than a finding.
- "Beat" has three parts: shorter, correct, and sufficient. Two are measurable and the third is a fact about the job.
- Eight lines of shell against thirty-five of Python, both running real Icarus simulations and agreeing on a passing and a failing testbench.
- Six lines of shell are correct on all four logs, so Python's twenty-nine extra lines buy no correctness at all — the short version is not a compromise.
- The naive four-line version does have Chapter 24.3's bug — it reports
FAILon a log whose onlyUVM_ERRORis the summary — and the fix is two lines ofgrep -v. - The real case against shell is length, not capability. An eight-line script can be verified by eye; a two-hundred-line one cannot, so the rule is a size limit rather than a language ban.
- The decision rule is three questions and two yeses: must it be called by another program, tested without the tool, or extended? Four of seven example tasks answer no to all three.
- The rule ignores how complicated the task is. Chapter 35.3's runner is Python because something calls it four hundred times, not because classifying a log is hard.
- A four-line Makefile rebuilt only when a source changed, and a Python equivalent must reimplement nine behaviours — of which a transitive dependency and a generated source that is itself a target are genuinely hard.
- The hand-rolled check raises
FileNotFoundErroron a missing source wheremakesays No rule to make target, which is the shape of every reimplementation of a mature tool. Its equal-timestamp behaviour happens to be right, which is luck rather than design. - Four of seven tasks can only be done in the tool's own language, because the data lives inside the tool's process and an external program reaches only the files the tool chose to write.
- Six lines of Tcl ask a question no report answers, and the vendor commands are shown as input a tool shell would run — never as captured output, since no such tool is installed here.
- The data-inside-the-tool question is first because it is forced rather than a trade-off.
- Every finding names a replacement. "Replace with a Makefile and keep Python for the leaves" is actionable; "this did not need to be Python" is a complaint.
- "Probably wrong" is an honest verdict. A wrapper that is too much today was early rather than wrong if next quarter something calls it four hundred times, and a reviewer cannot settle that.
That closes Module 36, and with it the Python track. The arc it ends on is worth stating plainly: the first chapters taught how to make a script work, the middle ones how to find out whether it does, and these last six how to tell — about somebody else's code, in an hour, with measurements rather than opinions.
Where this fits
Part of the Python curriculum.
