Skip to content
VLSI Mentor

Python Part II · Module 17 · Thinking Like a VLSI Automation Engineer

Separating What Touches the World From What Does Not

Every flow script does two incompatible kinds of work. Some of it reaches outside the process — opening logs, launching simulators, writing reports, reading the clock — and is slow, environment-dependent and awkward to exercise. The rest is pure transformation: text in, verdict out, the same answer every time. Both are necessary and they mix badly, because the moment a decision is made inside a function that also opens a file, that decision can only be checked by producing the file. Keeping them apart costs nothing while writing and is the single structural choice that decides whether the script can ever be tested.

Intermediate13 min readPython for VLSIScript DesignTestabilityMaintainabilityVerification Flow

Part II · Module 17 · Chapter 17.10 · Separating What Touches the World From What Does Not

1. The Engineering Problem

A colleague hands you a check script and asks whether it handles a truncated log correctly. To find out, you have to produce a truncated log — create a directory, write a partial file, run the script against it, read what it printed, and clean up. Five minutes for one case. There are six cases.

Now suppose the same script had a function that took a list of lines and returned a verdict. Answering the same question is one line in a Python prompt, and answering it for all six cases is a loop.

Nothing about the difficulty of the logic changed between those two situations. What changed is whether the logic can be reached without the filesystem, and that is decided by a single structural choice made when the script is written.

2. The Two Halves

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def classify(text): ...        # pure: testable in milliseconds
def check_log(path): ...       # touches the filesystem

Pure code takes values, returns values, and does nothing else. Given the same input it produces the same output, always, on any machine. It cannot fail because a disk is full or a path is wrong.

Impure code reaches outside the process: reading files, writing them, launching tools, reading the clock, asking the network. It is where a script's real-world failures live, and it is slow and awkward to exercise precisely because the world has to be arranged first.

Three blocks in a row joined by two arrows. On the left, the reading edge, which opens the log and is impure. An arrow labelled lines leads to the middle block, the pure core, which classifies and decides and touches nothing outside the process. An arrow labelled verdict leads to the right-hand block, the writing edge, which prints and sets the exit status and is impure.The reading edgeopens the log — impure,thinThe pure coredecides everything, touchesnothingThe writing edgeprints, sets the status —impurelinesverdict12
Figure 1 — the shape to aim for. The parts that touch the world sit at the edges and stay thin; everything that decides anything lives in the middle, where it can be reached without arranging a filesystem.

3. This Is Not the Same Split as Chapter 1.5

Chapter 1.5 separated processing from decision. This chapter separates pure from impure. Having just learned the first, it is natural to assume the second is a restatement, and it is not — the two cuts run in different directions:

PureImpure
Input—opening the log, reading the argument
Processingcounting errors, finding the marker—
Decisionapplying the verdict rule—
Outputformatting the summary stringprinting it, setting the exit status

Processing and decision are both pure. Input and output are both impure — except for the formatting, which is pure and is worth noticing: building a report string is a transformation, while emitting it touches the world. Separating those two is what lets you check a report's exact text without capturing standard output.

So the five-part model tells you what the parts mean; this chapter tells you what they touch. Both cuts are useful and they are independent.

4. Measuring the Difference

Here is the mixed version — one function doing both:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def check_log(path):
    """Impure: cannot be exercised without a real file."""
    with open(path) as f:
        lines = f.read().splitlines()
    errors = 0
    finished = False
    for line in lines:
        if "UVM_ERROR" in line:
            errors += 1
        if "Simulation complete" in line:
            finished = True
    if not finished:
        return "NO_FINISH"
    return "PASS" if errors == 0 else "FAIL"


print("signature takes:", "a path")
print("to test one case you must first create a file")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
signature takes: a path
to test one case you must first create a file

And the split version, where the decision logic no longer knows that files exist:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
END_MARKER = "Simulation complete"


def classify(lines):
    """Pure: lines in, verdict out. No filesystem, no clock, no tools."""
    if not lines:
        return "EMPTY_LOG"
    errors = 0
    finished = False
    for line in lines:
        if "UVM_ERROR" in line:
            errors += 1
        if END_MARKER in line:
            finished = True
    if not finished:
        return "NO_FINISH"
    return "PASS" if errors == 0 else "FAIL"


CASES = {
    "clean":     ["UVM_INFO ok", END_MARKER],
    "failed":    ["UVM_ERROR drv.sv(88)", END_MARKER],
    "truncated": ["UVM_INFO half way"],
    "empty":     [],
    "many":      ["UVM_ERROR a", "UVM_ERROR b", END_MARKER],
    "marker only": [END_MARKER],
}

for name in sorted(CASES):
    print(f"{name:<12} -> {classify(CASES[name])}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
clean        -> PASS
empty        -> EMPTY_LOG
failed       -> FAIL
many         -> FAIL
marker only  -> PASS
truncated    -> NO_FINISH

Six cases exercised in one program, with no directory created, no file written, no cleanup and nothing left behind. The whole thing runs in milliseconds and would run identically on any machine.

Doing the same for check_log means manufacturing six files first. That is not impossible — Module 33 shows how — but it is six times the setup, it is slower, and it can fail for reasons that have nothing to do with the logic being checked. Most engineers, facing that cost, check one case and assume the rest.

Note also what the split surfaced: marker only returns PASS. A log containing nothing but the end marker is reported clean, which may well be correct and is at least now a visible, decided behaviour rather than an untested corner.

5. The Signature Is the Tell

The rule that makes this checkable without reading a single function body:

A pure function must never take a path.

A parameter named path, filename or log_file means the function is going to open something, which means it cannot be exercised without a filesystem. A parameter named lines, text or records means it has been handed data and will do nothing but transform it.

This is reviewable at a glance:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
SIGNATURES = [
    ("classify(lines)", "pure"),
    ("check_log(path)", "impure -- opens a file"),
    ("count_errors(lines)", "pure"),
    ("write_report(path, rows)", "impure -- writes a file"),
    ("format_report(rows)", "pure -- builds the text only"),
    ("run_simulation(test, seed)", "impure -- launches a tool"),
]

for sig, kind in SIGNATURES:
    print(f"{sig:<28} {kind}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
classify(lines)              pure
check_log(path)              impure -- opens a file
count_errors(lines)          pure
write_report(path, rows)     impure -- writes a file
format_report(rows)          pure -- builds the text only
run_simulation(test, seed)   impure -- launches a tool

Note the pair near the bottom. format_report(rows) builds the text and write_report(path, rows) puts it somewhere — splitting those means the exact wording of a report can be checked against an expected string, which is the difference between a report that is reviewed and one that is glanced at.

The assembled shape is a thin shell around the pure core:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
END_MARKER = "Simulation complete"
EXIT_FOR = {"PASS": 0, "FAIL": 1, "NO_FINISH": 1, "EMPTY_LOG": 2}


def classify(lines):                      # pure
    if not lines:
        return "EMPTY_LOG"
    errors = 0
    finished = False
    for line in lines:
        if "UVM_ERROR" in line:
            errors += 1
        if END_MARKER in line:
            finished = True
    if not finished:
        return "NO_FINISH"
    return "PASS" if errors == 0 else "FAIL"


def format_verdict(verdict):              # pure
    return f"{verdict} (exit {EXIT_FOR[verdict]})"


def main(lines):                          # the impure shell, three lines
    verdict = classify(lines)
    print(format_verdict(verdict))
    return EXIT_FOR[verdict]


status = main(["UVM_ERROR drv.sv(88)", END_MARKER])
print("status would be:", status)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
FAIL (exit 1)
status would be: 1

Two pure functions holding every decision, and a main that reads, prints and exits. In a real script main would also open the file; it would still be about five lines.

6. The Impure Part Still Needs Testing

The honest limit, because this chapter can be over-sold.

Separating the halves does not make the impure half disappear or stop it failing. Opening a file is exactly where FileNotFoundError, a permissions problem, a wrong working directory and an encoding surprise all live, and those are real failures that matter.

What the separation buys is that the impure half becomes thin enough to review by inspection. A five-line main that opens a path, calls a pure function and exits can be read once and understood completely. A two-hundred-line function that opens files, decides things and prints as it goes cannot be reviewed that way by anybody.

So the goal is not purity for its own sake. It is to push everything that makes a judgment into code that can be exercised cheaply, and to leave the part that touches the world small enough that reading it is sufficient.

7. Why This Chapter Is Here

The boundary matters now, before any of the code exists, because it is almost free to establish and expensive to introduce later.

Writing classify(lines) instead of check_log(path) costs nothing on the day — it is the same logic with a different parameter. Retrofitting it into a three-hundred-line script that opens files in nine places is a day's work and a risk, which is why it usually does not happen.

Module 33 is about writing tests. Everything it can do depends on there being something testable to point at, and that is decided here. Chapter 17.11 traces the arc a script walks as it grows, and this boundary is the stage most scripts never reach.

8. The Impurity That Does Not Look Like One

Files and tools are obviously outside the process. Two other kinds of impurity hide inside functions that look self-contained, and both break the property that makes pure code worth having — that the same input produces the same answer.

The clock. A function calling datetime.now() returns something different every time it runs. It takes no path, opens nothing, and is still impure: you cannot write down its expected output, because the expected output depends on when you ask. A function that decides whether a run is "stale" by comparing its timestamp to now is doing two separable things — computing an age, and judging it — and only the second is pure. Pass the current time in as an argument and both halves become checkable.

Randomness. The same argument applies to anything that picks a seed, shuffles an order or samples a subset. Taking the random value as a parameter rather than generating it inside means the function can be exercised with a known one.

The general rule behind both: a function that reaches for a value rather than receiving it is impure, whatever the value is. The filesystem is the obvious case; the clock and the random source are the two that routinely slip through review because nothing in the signature suggests them.

There is a third, subtler case worth naming: a function that mutates its argument. Handed a list and appending to it, it returns one thing and changes another, and the caller's data is now different. It is pure by the "reaches for nothing" test and impure by the one that matters — you cannot call it twice and get the same result, because the first call changed the input. Chapter 16.6 covered mutation and this is the testability consequence.

The practical effect on how you write a signature is small and consistent. Anything the function needs from outside itself arrives as a parameter:

is_stale(run_time, now) rather than is_stale(run_time) pick_seed(rng) rather than pick_seed() with_tag(tags, tag) returning a new list rather than appending to the caller's

Each of those is one extra parameter and buys a function whose behaviour can be stated in a sentence and checked in a line. The caller — which is already impure, and already at the edge — is where the clock is read and the random value generated, which is exactly where the earlier sections put everything else that touches the world.

9. Exercises

Reasoning, with short programs.

Exercise 1 — Classify by signature

List every function in a script you have. For each, mark pure or impure from the signature alone, then check by reading the body. Note any you got wrong.

Exercise 2 — Six cases, no files

Take a decision function that currently opens a file and change it to take lines. Exercise it against six inputs in one program.

Exercise 3 — The two cuts

For a script of your own, fill in the two-by-two from Section 3. Say which cell is empty and why.

Exercise 4 — Split the formatting

Find a function that builds and prints a report. Split it into one that returns the text and one that emits it, then check the exact wording against an expected string.

Exercise 5 — Measure the shell

Count the lines in your script's impure part after separating. If it is more than about ten, find what else can move inward.

Exercise 6 — The untested corner

Exercising Section 4's classify surfaced marker only -> PASS. Find an equivalent corner in your own logic that you had not considered, and decide whether its behaviour is right.

What the impure edge is talking to:

  • GLS in Regression — the run directories and logs the reading edge opens, and the states they arrive in.
  • UVM Report Server — the producer of the text the pure core parses.

What the pure core enables:

  • Automating GLS Triage — triage logic worth checking case by case, which requires it to be reachable without a farm.
  • First-Mismatch Triage — a decision rule precise enough to be worth testing against known inputs.

11. Summary

  • Pure code takes values and returns values; impure code reaches outside the process.
  • The split is by what the code touches, which is independent of Chapter 1.5's split by what it means.
  • Processing and decision are both pure; input and output are both impure — except formatting, which is pure.
  • A pure function can be exercised against six inputs in one program, with no files, no cleanup and no environment.
  • The same check on an impure function means manufacturing six filesystem states, which is why most people check one and assume.
  • The signature is the tell: a parameter named path means impure, a parameter named lines means pure.
  • Splitting format_report from write_report lets a report's exact wording be checked rather than glanced at.
  • The impure part still fails and still needs testing — the goal is to make it thin enough to review by inspection.
  • Five lines of shell can be read once and understood; two hundred mixed lines cannot be reviewed by anybody.
  • The clock and the random source are impurities that do not look like one — nothing in the signature suggests them.
  • A function that reaches for a value rather than receiving it is impure, whatever the value is.
  • A function that mutates its argument cannot be called twice with the same result, which is the testability consequence of Chapter 16.6.
  • Pass the time, the random source and new collections in and out — one extra parameter buys a checkable function.
  • Establishing the boundary costs nothing on the day and is a day's work to retrofit, which is why it usually never happens.

The next chapter follows a script through the stages it passes on the way from a hard-coded path to something a team depends on.

Where this fits

Part of the Python curriculum.