Python Part II · Module 20 · Simulation and UVM Log Parsing
Anatomy of a Script: Input, Processing, Decision, Output
This module builds one real tool, from a five-line check into a parser that streams a multi-gigabyte log and returns a structured record. Before the first line of it, the shape is worth naming — because every flow script in a verification team has the same four parts, and the two in the middle are routinely fused into one loop. Fusing them is not a bug; the script prints the right answer. It is the reason that script cannot later report how many errors there were, cannot be re-pointed at a different pass rule, and cannot be tested without capturing what it printed.
Foundation15 min readPythonPart IILog ParsingVerification FlowUVM
Part II · Module 20 · Chapter 20.1 · Anatomy of a Script: Input, Processing, Decision, Output
1. The Same Four Parts, Every Time
A script that decides whether a simulation passed. A script that checks a filelist. A script that compares two synthesis reports. A script that sweeps a seed range. They look like four different programs and they have the same skeleton:
- Input — something arrives from outside the script: a file, an argument, an environment variable.
- Processing — the raw text becomes facts: a count, a name, a time, a list of failures.
- Decision — the facts become a verdict, by a rule somebody chose.
- Output — the verdict and the facts leave the script, for a human or for the next tool.
Chapter 17.4 gave a five-part model: Problem, Input, Processing, Decision, Output. The part missing here is Problem, and it is missing for a good reason — Problem is settled before any code exists and leaves no line behind in the file. These four are the parts you can point at in the source.
2. Named on a Real Check
The simplest useful question in verification: did this simulation pass?
UVM_INFO @ 0: reporter [RNTST] Running test axi_smoke_test...
UVM_WARNING mon.sv(51) @ 99300: uvm_test_top.env.agt.mon [SMP] late sample ignored
UVM_ERROR drv.sv(88) @ 141900: uvm_test_top.env.agt.drv [WR] bad handshake
UVM_INFO seq.sv(140) @ 200000: uvm_test_top.env.agt.seqr [SEQ] axi_wr_rd_seq donedef count_errors(lines):
"""How many UVM_ERROR messages are in these lines."""
n = 0
for line in lines:
if line.startswith("UVM_ERROR"):
n += 1
return n
with open("sim_small.log", encoding="utf-8") as handle: # input
lines = handle.read().splitlines()
errors = count_errors(lines) # processing
passed = errors == 0 # decision
print("PASS" if passed else "FAIL") # output
print("errors:", errors)FAIL
errors: 1Four lines at the bottom, four parts, one comment each. That is the canonical shape this whole module is built from:
log = open("sim.log") # input
errors = count_errors(log) # processing
passed = errors == 0 # decision
print("PASS" if passed else "FAIL") # outputNote that the annotation above uses a bare open() and the executed version uses with. Chapter 15.5 settled that, and the rule has not relaxed: the four-line sketch is how you think about the script; with is how you write it. Every executed example in this module opens files inside a with.
3. Each Part Fails Differently
Naming the parts is worth something the moment a script goes wrong, because the four failures do not look alike:
| Part | What a failure looks like | Where to look |
|---|---|---|
| Input | FileNotFoundError, an empty read, the wrong run directory | the path, not the parser |
| Processing | the right verdict for the wrong reason — a count that is off | the pattern and the data |
| Decision | a defensible-looking verdict nobody agrees with | the rule, which is a choice |
| Output | the answer is right and nothing downstream can read it | the format, not the logic |
The two outer parts fail loudly. The two inner ones fail by returning something plausible, which is the shape this curriculum keeps returning to — and the reason it matters which of the two is at fault is that only one of them is a bug.
A wrong count is a defect. A verdict somebody disagrees with is usually not: it is a policy nobody wrote down. Those need different conversations, and a script that keeps them in separate lines lets you have the right one.
4. Facts Are Not Verdicts
Here is the split doing real work. One processing step, two policies:
LINES = [
"UVM_INFO @ 0: reporter [RNTST] Running test axi_smoke_test...",
"UVM_WARNING mon.sv(51) @ 99300: uvm_test_top.env.agt.mon [SMP] late sample",
"UVM_ERROR drv.sv(88) @ 141900: uvm_test_top.env.agt.drv [WR] bad handshake",
"UVM_ERROR drv.sv(104) @ 180400: uvm_test_top.env.agt.drv [WR] timeout",
"UVM_INFO seq.sv(140) @ 200000: uvm_test_top.env.agt.seqr [SEQ] done",
]
def count(lines, marker):
"""How many lines begin with this marker."""
n = 0
for line in lines:
if line.startswith(marker):
n += 1
return n
errors = count(LINES, "UVM_ERROR") # processing — a fact
warnings = count(LINES, "UVM_WARNING") # processing — a fact
print("errors :", errors)
print("warnings:", warnings)
print("strict :", "PASS" if errors == 0 else "FAIL")
print("lenient :", "PASS" if errors <= 2 and warnings <= 5 else "FAIL")errors : 2
warnings: 1
strict : FAIL
lenient : PASSTwo errors and one warning is a fact. Whether that is a passing run is not — it depends on whether this is a signoff gate or a nightly smoke sweep with two known-failing tests. The counting code is identical in both cases, and the rule is one expression.
That is why the two middle parts are listed separately. They do not merely happen in sequence; they change for different reasons. The processing changes when the tool's output format changes. The decision changes when the project's standards change. On a real flow those two events are months apart and are decided by different people.
5. The Fused Version Is Not Wrong
This is what most people write first, and it deserves a fair hearing:
LINES = [
"UVM_INFO @ 0: reporter [RNTST] Running test axi_smoke_test...",
"UVM_WARNING mon.sv(51) @ 99300: uvm_test_top.env.agt.mon [SMP] late sample",
"UVM_ERROR drv.sv(88) @ 141900: uvm_test_top.env.agt.drv [WR] bad handshake",
"UVM_ERROR drv.sv(104) @ 180400: uvm_test_top.env.agt.drv [WR] timeout",
"UVM_INFO seq.sv(140) @ 200000: uvm_test_top.env.agt.seqr [SEQ] done",
]
for line in LINES:
if line.startswith("UVM_ERROR"):
print("FAIL")
break
else:
print("PASS")FAILSix lines instead of fourteen, the same verdict, and it stops at the first error instead of reading the whole file — which on a 20 GB log is Chapter 10.7's argument and a real advantage.
So it is not a bug. Here is what it costs, precisely:
It cannot tell you how many. The count never exists as a value, so there is nothing to put in a report, nothing to compare against yesterday, and nothing to aggregate over a thousand runs. Adding that is not an edit — it is a rewrite of the loop.
It cannot be re-pointed at a new policy. "Fail only above two errors" or "fail on warnings too" means restructuring, because the rule is spread across a break, a print and an else.
It cannot be tested without capturing what it printed. The verdict only ever exists as text on stdout. Chapter 19.18's whole approach — a list of inputs and expected values — needs a function that returns something.
The for/else is doing work most readers will not see. Chapter 10.8 covers it; it runs only when the loop finished without breaking. In a six-line script that is fine. Three nested loops later it is where a reviewer stops reading.
Use the fused form for a check you will run once, from the command line, today. Use the four parts for anything that will still be running next quarter — which, in a verification flow, is almost everything.
6. Where the Parts Live in a File
The four parts are not four functions. They are a layout, and the usual shape puts the facts in functions and leaves the rest at the bottom:
MARKER = "UVM_ERROR"
def read_lines(path):
"""Input: the log as a list of lines."""
with open(path, encoding="utf-8") as handle:
return handle.read().splitlines()
def count_errors(lines):
"""Processing: how many error messages. A fact, not a verdict."""
n = 0
for line in lines:
if line.startswith(MARKER):
n += 1
return n
def verdict(errors):
"""Decision: the project's rule, in one place."""
return "PASS" if errors == 0 else "FAIL"
LINES = [
"UVM_INFO @ 0: reporter [RNTST] Running test axi_smoke_test...",
"UVM_ERROR drv.sv(88) @ 141900: uvm_test_top.env.agt.drv [WR] bad handshake",
"UVM_INFO seq.sv(140) @ 200000: uvm_test_top.env.agt.seqr [SEQ] done",
]
errors = count_errors(LINES)
print(f"{verdict(errors)} errors={errors}") # outputFAIL errors=1Three functions and two lines. read_lines is unused in this run because the lines are a literal — which is exactly the point of separating it: the processing and the decision can be exercised without a file existing at all. That is what makes Chapter 19.18's sample-list testing possible, and it is the first reason this layout is worth the extra lines.
Note the one-line output combining verdict and facts. A verdict with no number is hard to act on; a number with no verdict is hard to scan. Both, on one line, is what a nightly summary wants — Chapter 20.16 turns that into a structured record.
7. Predict, Run, Observe
LINES = [
"UVM_INFO @ 0: reporter [RNTST] starting",
"UVM_ERROR drv.sv(88) @ 141900: bad handshake",
"UVM_WARNING mon.sv(51) @ 99300: late sample",
]
def count(lines, marker):
n = 0
for line in lines:
if line.startswith(marker):
n += 1
return n
e = count(LINES, "UVM_ERROR")
w = count(LINES, "UVM_WARNING")
# A
print("A:", e, w)
# B
print("B:", "PASS" if e == 0 else "FAIL")
# C
print("C:", "PASS" if e <= 1 else "FAIL")
# D
print("D:", count(LINES, "UVM_"))
# E
print("E:", count(LINES, "drv.sv"))
# F
print("F:", type(e).__name__, type(e == 0).__name__)The verified output:
A: 1 1
B: FAIL
C: PASS
D: 3
E: 0
F: int boolA — two facts from one function, which is what makes a second policy free.
B and C — the same fact, two rules, two verdicts. Nothing was recounted.
D — startswith("UVM_") matches all three severities. The marker is an argument, so the same processing answers a different question.
E — zero, because startswith anchors at position 0 and drv.sv is in the middle. Chapter 19.11's match versus search distinction, in the string-method form.
F — processing returned an int and the decision produced a bool. The return type is the test from Section 4: a count is a fact, a boolean is a verdict.
8. Exercises
Exercise 1 — label a script. Take any script you have written and mark each line with one of the four parts. Note any line you cannot label and say why.
Exercise 2 — the missing part. Find or write a script with only three of the four parts. Say which is absent and why that is correct rather than a gap.
Exercise 3 — two policies. Write a processing step that produces two counts, then two verdict lines applying different rules. Make them disagree, and say which project would want each.
Exercise 4 — the leak test. Write a function called check_errors(lines, threshold) that returns True or False. Say what has leaked into it, and split it into two.
Exercise 5 — the fused version. Write the for/else form for a check of your own. Then try to add "report the count as well" and record what you had to change.
Exercise 6 — the test boundary. Take your separated version and call its processing function on a list of literal lines, with no file. Say what that makes possible that the fused version does not.
Exercise 7 — four failures. For a checker of your own, describe what a failure in each of the four parts would look like to the person reading the output. Say which two would be noticed immediately.
Exercise 8 — fact or verdict. For each, say which and why: a line count; True; the first failing line; "TIMEOUT"; a list of assertion names; an exit code; the simulation time of the first error.
9. Summary
- Every flow script has the same four parts: input, processing, decision, output.
- Chapter 17.4's model has five; the one missing here is Problem, because Problem leaves no line behind in the file.
- Processing produces facts — counts, names, times, lists. Decision applies policy — a rule somebody chose.
- They change for different reasons: processing when the tool's output format changes, decision when the project's standards change.
- The same two counts can be PASS under one policy and FAIL under another, with no recounting.
- The return type is the test: a count, a name or a list is a fact; a
True, a"PASS"or an exit code is a verdict. - A threshold argument or a boolean return in a counting function means policy has leaked into measurement.
- Input and output fail loudly; processing and decision fail by returning something plausible — and only one of those two is a bug.
- The fused
for/elseversion is not wrong. It is shorter, it stops early, and it prints the right answer. - What it costs: no count to report, no way to change the rule without restructuring, and nothing to test but stdout.
- The usual layout puts input and processing in functions and leaves the decision and output at the bottom, which lets the processing be exercised with no file at all.
- Four named parts is a shape, not a quota — do not invent a decision a script does not have.
- The sketch uses
open(); the code useswith. Chapter 15.5's rule has not relaxed.
The next chapter writes the real thing: five lines that read a simulation log and print a verdict an engineer would actually act on.
Where this fits
Part of the Python curriculum.
