Skip to content
VLSI Mentor

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.

Four steps in sequence. One, input: something arrives from outside the script, such as a log file or an argument. Two, processing: the raw text becomes facts, such as counts, names and times — measurements, not conclusions. Three, decision: the facts become a verdict by a rule somebody chose, which is policy and not measurement. Four, output: the verdict and the facts leave the script, for a human to read and for the next tool to consume.Four parts, and what each one owns01INPUTa log, an argument, the world02PROCESSINGtext becomes facts — counts, times03DECISIONfacts become a verdict — policy04OUTPUTfor a human and for the next tool
Figure 1 — the four parts, and what each one is responsible for. The two in the middle are the ones worth keeping apart: one produces facts, the other applies a rule to them, and they change for entirely different reasons.

2. Named on a Real Check

The simplest useful question in verification: did this simulation pass?

Azvya Education Pvt. Ltd.VLSI Mentor
sim_small.log
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 done
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def 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)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
FAIL
errors: 1

Four lines at the bottom, four parts, one comment each. That is the canonical shape this whole module is built from:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
log = open("sim.log")                  # input
errors = count_errors(log)             # processing
passed = errors == 0                   # decision
print("PASS" if passed else "FAIL")    # output

Note 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:

PartWhat a failure looks likeWhere to look
InputFileNotFoundError, an empty read, the wrong run directorythe path, not the parser
Processingthe right verdict for the wrong reason — a count that is offthe pattern and the data
Decisiona defensible-looking verdict nobody agrees withthe rule, which is a choice
Outputthe answer is right and nothing downstream can read itthe 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
errors  : 2
warnings: 1
strict  : FAIL
lenient : PASS

Two 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
FAIL

Six 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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}")      # output
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
FAIL  errors=1

Three 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A: 1 1
B: FAIL
C: PASS
D: 3
E: 0
F: int bool

A — 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/else version 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 uses with. 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.