Python Part II · Module 17 · Thinking Like a VLSI Automation Engineer
A Script That Grows: From One File to a Shape
A script that a team depends on and a four-line throwaway are frequently the same script, eighteen months apart. Nobody sat down to write the dependable version; it arrived one small change at a time, and the changes have an order that is remarkably consistent across projects and people. Seeing that order laid out is useful for two reasons. It tells you what the next sensible step is for something you have, and — more usefully — it tells you when to stop, because the most common mistake after learning all six stages is applying them to a script that needed two.
Intermediate12 min readPython for VLSIScript DesignMaintainabilityEngineering JudgmentVerification Flow
Part II · Module 17 · Chapter 17.11 · A Script That Grows: From One File to a Shape
1. The Same Script, Six Times
One task throughout this chapter: decide whether a simulation run was clean. It appears six times, and the output never changes. Every version prints the same verdict for the same input.
That is the point worth holding onto. None of the stages make the script better at its job. They make it better at being used by somebody else, run unattended, called twice, or changed in a year — and if none of those things is going to happen, the later stages are cost without benefit.
2. Stages One and Two
# stage 1: hard-coded path
# stage 2: variable
# stage 3: function
# stage 4: argument + exit statusStage 1 — hard-coded. Everything is a literal. This is where every script starts and it is the correct place to start, because at this point you are still finding out what the task actually is.
LINES = ["UVM_INFO ok", "UVM_ERROR drv.sv(88)", "Simulation complete"]
errors = 0
for line in LINES:
if "UVM_ERROR" in line:
errors += 1
if errors == 0:
print("PASS")
else:
print("FAIL")FAILFour lines of logic and no structure at all. It works, and for a question you are answering once this afternoon it is the right amount of engineering.
Stage 2 — a variable. The trigger is concrete: you edited the same literal twice. The marker, the path, the error string — whatever you keep changing moves to the top where it can be changed in one place.
LINES = ["UVM_INFO ok", "UVM_ERROR drv.sv(88)", "Simulation complete"]
ERROR_MARKER = "UVM_ERROR"
END_MARKER = "Simulation complete"
errors = 0
for line in LINES:
if ERROR_MARKER in line:
errors += 1
print("FAIL" if errors else "PASS")FAILSame output. What changed is that the thing most likely to need changing — the marker a tool upgrade might rename — is now visible at the top rather than buried in a comparison.
3. Stage Three: a Function
The trigger is needing the logic in two places. Usually the second place is a second script, and occasionally it is the realisation that you cannot describe what the code does without naming it.
ERROR_MARKER = "UVM_ERROR"
END_MARKER = "Simulation complete"
def classify(lines):
errors = 0
finished = False
for line in lines:
if ERROR_MARKER in line:
errors += 1
if END_MARKER in line:
finished = True
if not finished:
return "NO_FINISH"
return "PASS" if errors == 0 else "FAIL"
print(classify(["UVM_INFO ok", "UVM_ERROR drv.sv(88)", END_MARKER]))FAILSame output again, and three things became possible that were not before. The logic has a name, so it can be discussed. It returns rather than printing, so a caller can do something other than display it. And it can be called twice.
This is also the stage where Chapter 1.11's boundary appears: classify takes lines, not a path, and is therefore pure. That was free to decide here and expensive to retrofit later.
A great many scripts should stop here, and Section 6 is about recognising that.
4. Stage Four: Argument and Exit Status
The trigger is that the script runs when nobody is watching — from a Makefile, a nightly wrapper, a job scheduler.
Two things arrive together, which is why the canonical list pairs them. The input stops being baked in, and the verdict becomes visible to a program:
import sys
ERROR_MARKER = "UVM_ERROR"
END_MARKER = "Simulation complete"
EXIT_FOR = {"PASS": 0, "FAIL": 1, "NO_FINISH": 1}
def classify(lines):
errors = 0
finished = False
for line in lines:
if ERROR_MARKER 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 main(lines):
verdict = classify(lines)
print(verdict)
return EXIT_FOR[verdict]
status = main(["UVM_INFO ok", "UVM_ERROR drv.sv(88)", END_MARKER])
print("exit status would be:", status)
sys.exit(status)FAIL
exit status would be: 1In a real script main would read the path from sys.argv and open the file; the structure is otherwise exactly this. Chapter 1.9 covered why the exit line matters, and Module 22 covers argument handling properly.
Note that classify is untouched. Each stage so far has added a layer around the previous one rather than rewriting it, which is what makes the progression cheap.
5. Stages Five and Six
Stage 5 — failure states. The trigger is the first bad night: something arrived that the script had not considered, and it reported a verdict it had not earned.
ERROR_MARKER = "UVM_ERROR"
END_MARKER = "Simulation complete"
EXIT_FOR = {"PASS": 0, "FAIL": 1, "NO_FINISH": 1, "EMPTY_LOG": 2, "NO_LOG": 2}
def classify(lines, exists=True):
if not exists:
return "NO_LOG"
if not lines:
return "EMPTY_LOG"
errors = 0
finished = False
for line in lines:
if ERROR_MARKER in line:
errors += 1
if END_MARKER in line:
finished = True
if not finished:
return "NO_FINISH"
return "PASS" if errors == 0 else "FAIL"
for name, lines, exists in [
("clean", ["UVM_INFO ok", END_MARKER], True),
("failed", ["UVM_ERROR x", END_MARKER], True),
("cut", ["UVM_INFO ok"], True),
("empty", [], True),
("missing", [], False),
]:
v = classify(lines, exists)
print(f"{name:<8} {v:<10} exit {EXIT_FOR[v]}")clean PASS exit 0
failed FAIL exit 1
cut NO_FINISH exit 1
empty EMPTY_LOG exit 2
missing NO_LOG exit 2This is Chapter 1.8's table, arriving in the script's life at the point where experience demanded it. Notice that it is still the same classify, with two guards added at the top.
Stage 6 — tests. The trigger is that somebody else depends on it, which usually means you are no longer the person who will debug it.
Stage 6 is only reachable because stage 3 made classify pure. The check is a handful of assertions over the table above, with no files involved:
END_MARKER = "Simulation complete"
def classify(lines, exists=True):
if not exists:
return "NO_LOG"
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"
CHECKS = [
(["UVM_INFO ok", END_MARKER], True, "PASS"),
(["UVM_ERROR x", END_MARKER], True, "FAIL"),
(["UVM_INFO ok"], True, "NO_FINISH"),
([], True, "EMPTY_LOG"),
([], False, "NO_LOG"),
]
failures = 0
for lines, exists, expected in CHECKS:
got = classify(lines, exists)
if got != expected:
failures += 1
print(f" MISMATCH expected {expected} got {got}")
print(f"{len(CHECKS) - failures} of {len(CHECKS)} checks passed")5 of 5 checks passedModule 33 does this properly with a framework. The shape is already here, and it took five lines because the logic was reachable.
6. Most Scripts Should Stop Early
The warning this chapter needs, because six stages invite completion.
Stage 2 is the right resting place for a throwaway. A script you run this afternoon and delete does not need a function.
Stage 3 is where a large fraction of useful flow scripts belong permanently. Named logic, returns a value, runs when you ask it to. If it never runs unattended there is no wrapper to tell, and stage 4 buys nothing.
Stage 4 is the real threshold, and it is the one people under-reach rather than over-reach. The moment a script is in a Makefile or a cron job, the exit status stops being optional — and that transition usually happens without anybody editing the script, which is exactly why it gets missed.
Stages 5 and 6 are for tools a team depends on. They cost real time and they are worth it for a small number of scripts.
The failure mode in both directions is worth naming. Applying stage 6 to a twenty-line helper is the over-engineering Chapter 1.4 warned about. Leaving a nightly regression summariser at stage 3 is how a green dashboard hides eleven failures.
7. Which Stage Are You At?
A short diagnostic, which is the practical use of the whole chapter:
| If this is true | You are at | The next sensible step |
|---|---|---|
| the path is a literal in the body | 1 | move it to the top if you edit it again |
| constants are at the top, logic is inline | 2 | name the logic when you need it twice |
| the logic is a function that returns | 3 | stop, unless it runs unattended |
| it runs from a Makefile or cron | 3 | urgent: add the exit status |
| it has an exit status but one failure path | 4 | enumerate the failure states |
| other people's work depends on it | 5 | write the checks |
The two rows worth re-reading are the two that mention running unattended. A script at stage 3 that has quietly acquired a cron entry is the most common dangerous configuration in a flow, because nothing about the script changed — its context did.
8. Where Each Stage Is Taught
The arc is also a map of the curriculum, which is this chapter's second job:
- Stage 1–2 — Modules 2 to 5: variables, strings, numbers, constants by convention.
- Stage 3 — Module 11: functions, return values, and the discipline of naming.
- Stage 4 — Module 22 for arguments, and Chapter 1.9 for the exit status.
- Stage 5 — Modules 14 and 15: errors, and the missing, empty and truncated states.
- Stage 6 — Module 33: tests, made possible by Chapter 1.11's boundary.
Everything between here and there is in service of a script that already works getting to the next stage without being rewritten.
9. The Arc Also Runs Backwards
Every stage so far has added something, which makes the progression sound one-way. It is not, and the reverse move is worth as much as the forward one because nobody ever schedules it.
Scripts outlive their reasons. A stage-5 failure table was written because a specific thing kept happening; two years later the tool that produced that failure has been replaced, and four branches handle a case that can no longer occur. They are not harmless — every reader of the function has to understand them before concluding they do not matter, and every change has to preserve them.
Generality added for a second caller that never arrived. A script given a --format flag because somebody might want JSON, which nobody ever used, is now a script with two output paths, one of which has never been exercised and is therefore almost certainly broken. Deleting it is a strict improvement and removes an untested path from the codebase.
Stages applied without their trigger. Section 7's table is as useful read right to left: a twenty-line helper with a test suite, a config file and three levels of indirection is a stage-6 script doing a stage-2 job, and the right move is to simplify rather than maintain.
The question to ask periodically, and particularly when inheriting something, is which of these stages is still earning its place? Concretely: does any caller use that flag; has that failure state occurred in the last year; is the abstraction serving two cases or one.
Removing a stage is harder than adding one, for an asymmetry worth naming. Adding is additive and safe — the old behaviour still works. Removing requires proving nothing depends on what you are deleting, and in a flow where scripts call scripts, that proof is genuinely difficult. Which is the real argument for not advancing a stage without its trigger: the cost is not the afternoon of adding it, it is that removing it later will never quite be worth the risk, so it stays forever.
A script that stopped at stage 3 because nothing pushed it further is in better shape than one driven to stage 6 by enthusiasm. The first is small and right-sized; the second is a maintenance obligation somebody took on for a benefit nobody asked for.
10. Exercises
Reasoning, with short programs.
Exercise 1 — Locate a script
Take three scripts from your flow and place each at a stage using Section 7's table. Note any that are at stage 3 with a cron entry.
Exercise 2 — Walk one stage
Advance one of them by exactly one stage. Confirm the output is unchanged, which is the test that you refactored rather than rewrote.
Exercise 3 — Find the trigger
For each advance you made, name the event that justified it. If you cannot name one, consider reverting.
Exercise 4 — The unattended check
Find every script in your flow that runs from a Makefile, cron or CI. For each, check whether it sets an exit status.
Exercise 5 — Five checks
Write Section 5's check loop for a decision function of your own. Count how long it took, and say whether it was possible without the function being pure.
Exercise 6 — Argue for stopping
Pick a script that should stay at stage 2 or 3 forever and write the one-sentence justification you would give in review.
11. Related Tutorials
What pushes a script up the stages:
- GLS in Regression — the unattended context that makes stage 4 mandatory rather than optional.
- Automating GLS Triage — a task whose consumers make stages 5 and 6 worth the time.
What it costs to stop too early:
- Coverage Closure and Signoff — decisions taken on script output, where an unreported failure state becomes a signoff problem.
- First-Mismatch Triage — the kind of logic that earns tests.
12. Summary
- The same task appears at six stages and the output never changes — the stages improve how the script can be used, not what it does.
- Stage 1, hard-coded, is the correct place to start while you are still learning the task.
- Stage 2's trigger is editing the same literal twice.
- Stage 3's trigger is needing the logic in two places, and it buys a name, a return value and reusability.
- Chapter 1.11's pure boundary appears at stage 3 and is free there, expensive later.
- Stage 4's trigger is running unattended, and the argument and the exit status arrive together.
- Stage 5's trigger is the first bad night; stage 6's is somebody else depending on it.
- Each stage wraps the previous one rather than rewriting it, which is what makes the progression cheap.
- Most scripts should stop at stage 3, and applying stage 6 to a twenty-line helper is over-engineering.
- A stage-3 script with a cron entry is the most common dangerous configuration, because the script did not change — its context did.
- The arc runs backwards too: failure branches for tools that no longer exist, and flags no caller ever used, are worth deleting.
- Removing a stage is harder than adding one, because removal requires proving nothing depends on it.
- That asymmetry is the real argument against advancing without a trigger — what you add will likely never be removed.
The last chapter of this module applies everything so far in reverse: given somebody else's two hundred lines, find its inputs, outputs, decision and failure behaviour.
Where this fits
Part of the Python curriculum.
