Skip to content
VLSI Mentor

Python Part II · Module 32 · Python, Tcl and Bash in EDA Flows

Tool-Native Tcl vs Python-Owned Flow Logic

Chapter 32.4 generated Tcl and left one question open: how much should the generated script contain? The answer depends on a line that is usually drawn by instinct, and this chapter draws it with a measurement instead. A Verilog elaborator is available here, so a Python re-implementation of a tool's most basic query can be compared against what the tool would actually report — and the gap turns out not to be a matter of effort. The information the query returns does not exist in the source text at all.

Advanced21 min readPythonPart IIAutomationTclEDA Tools

Part II · Module 32 · Chapter 32.5 · Tool-Native Tcl vs Python-Owned Flow Logic

1. The Temptation

Having built the generator in Chapter 32.4, the next thought is reasonable and wrong:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
THOUGHTS = [
    ("I generate the Tcl, so I know what the design contains",
     "no — the spec is the input, not the design"),
    ("get_cells is just a pattern match over instance names",
     "it is a query against an elaborated hierarchy — §3"),
    ("I could parse the netlist and skip the tool",
     "measured in §3: 1 against 8"),
    ("Python has better string handling than Tcl",
     "true, and irrelevant to this question"),
]

print(f"{'the thought':<52} the answer")
for t, a in THOUGHTS:
    print(f"{t:<52} {a}")

print()
print("thoughts that are true:", 1)
print("thoughts that are true AND relevant:", 0)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
the thought                                          the answer
I generate the Tcl, so I know what the design contains no — the spec is the input, not the design
get_cells is just a pattern match over instance names it is a query against an elaborated hierarchy — §3
I could parse the netlist and skip the tool          measured in §3: 1 against 8
Python has better string handling than Tcl           true, and irrelevant to this question

thoughts that are true: 1
thoughts that are true AND relevant: 0

The last row is the one worth naming, because it is the argument people actually make. Python is better at strings than Tcl, and the question is not which language handles text better — it is which process holds the data.

2. The Annotation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# inside the tool: get_cells, report_timing, set_clock_groups
# outside: which corners to run, and what to do with the reports
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
INSIDE = ["get_cells", "report_timing", "set_clock_groups"]
OUTSIDE = ["which corners to run", "what to do with the reports"]

print("inside the tool:")
for c in INSIDE:
    print(f"  {c}")
print("outside:")
for c in OUTSIDE:
    print(f"  {c}")

print()
print("items the annotation places:", len(INSIDE) + len(OUTSIDE))
print("the reason it gives       :", "none")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
inside the tool:
  get_cells
  report_timing
  set_clock_groups
outside:
  which corners to run
  what to do with the reports

items the annotation places: 5
the reason it gives       : none

Five items, correctly placed, with no stated reason — which is exactly the situation Chapter 31.1 was in with "do not generate logic". The placements are right and a reader cannot apply them to a sixth item.

Note the three inside items are three different kinds of thing: get_cells reads the database, report_timing computes over it, and set_clock_groups changes it. All three are inside for the same reason, which Section 3 measures.

3. A Regex get_cells Against a Real Elaborator

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import re
import subprocess
from pathlib import Path

NETLIST = """module leaf #(parameter ID = 0) (input a, output b);
  assign b = ~a;
  initial $display("  elaborated leaf ID=%0d", ID);
endmodule

module top #(parameter N = 8) (input [7:0] a, output [7:0] b);
  genvar i;
  generate
    for (i = 0; i < N; i = i + 1) begin : bank
      leaf #(.ID(i)) u_leaf (.a(a[i]), .b(b[i]));
    end
  endgenerate
  // leaf u_commented (.a(a[0]), .b(b[0]));
`ifdef EXTRA_LEAF
  leaf #(.ID(99)) u_ifdef (.a(a[0]), .b());
`endif
endmodule

module tb;
  reg [7:0] a = 8'h00; wire [7:0] b;
  top #(8) u_top(.a(a), .b(b));
  initial begin #1 $finish; end
endmodule
"""
Path("net.v").write_text(NETLIST, encoding="utf-8")

CELL = re.compile(r"^\s*(\w+)\s*(?:#\([^)]*\))?\s*(\w+)\s*\(", re.M)
SKIP = {"module", "generate", "for", "initial", "if"}

src = Path("net.v").read_text(encoding="utf-8")
found = [(m.group(1), m.group(2)) for m in CELL.finditer(src)
         if m.group(1) not in SKIP]

print("what a regex over the source finds:")
for mod, inst in found:
    print(f"  {mod:<8} {inst}")
print("  count:", len(found))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
what a regex over the source finds:
  top      u_top
  count: 1

One instance. Now ask the elaborator:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import subprocess
from pathlib import Path

NETLIST = """module leaf #(parameter ID = 0) (input a, output b);
  assign b = ~a;
  initial $display("  elaborated leaf ID=%0d", ID);
endmodule

module top #(parameter N = 8) (input [7:0] a, output [7:0] b);
  genvar i;
  generate
    for (i = 0; i < N; i = i + 1) begin : bank
      leaf #(.ID(i)) u_leaf (.a(a[i]), .b(b[i]));
    end
  endgenerate
  // leaf u_commented (.a(a[0]), .b(b[0]));
`ifdef EXTRA_LEAF
  leaf #(.ID(99)) u_ifdef (.a(a[0]), .b());
`endif
endmodule

module tb;
  reg [7:0] a = 8'h00; wire [7:0] b;
  top #(8) u_top(.a(a), .b(b));
  initial begin #1 $finish; end
endmodule
"""
Path("net.v").write_text(NETLIST, encoding="utf-8")


def elaborate(defines=()):
    args = ["iverilog", "-g2005"] + [f"-D{d}" for d in defines] + \
           ["-o", "n.vvp", "net.v"]
    c = subprocess.run(args, stdout=subprocess.PIPE,
                       stderr=subprocess.STDOUT, text=True)
    if c.returncode:
        return None
    r = subprocess.run(["vvp", "n.vvp"], stdout=subprocess.PIPE,
                       stderr=subprocess.STDOUT, text=True)
    return [l for l in r.stdout.splitlines() if "elaborated leaf" in l]


plain = elaborate()
print("what the elaborator creates:")
for l in plain:
    print(l)
print("  count:", len(plain))

extra = elaborate(["EXTRA_LEAF"])
print()
print("with -DEXTRA_LEAF:", len(extra))
print("the regex's answer :", 1, "in both cases")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
what the elaborator creates:
  elaborated leaf ID=0
  elaborated leaf ID=1
  elaborated leaf ID=2
  elaborated leaf ID=3
  elaborated leaf ID=4
  elaborated leaf ID=5
  elaborated leaf ID=6
  elaborated leaf ID=7
  count: 8

with -DEXTRA_LEAF: 9
the regex's answer : 1 in both cases

One against eight, and nine when a define changes the design while the regex's answer does not move.

Four separate reasons the text and the hierarchy disagree, and each is ordinary:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
REASONS = [
    ("the generate loop", "8 instances from 1 line",
     "the count is a parameter, resolved at elaboration"),
    ("the commented instance", "regex may find it, design has none",
     "comments are not part of the design"),
    ("the `ifdef instance", "present or absent per define",
     "the preprocessor runs before elaboration"),
    ("the parameter override", "top #(8) could be #(64)",
     "the instantiation decides, not the module"),
]

print(f"{'why they differ':<26} {'effect':<36} because")
for what, effect, why in REASONS:
    print(f"{what:<26} {effect:<36} {why}")

print()
print("reasons:", len(REASONS))
print("reasons a better regex could fix:", 0)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
why they differ            effect                               because
the generate loop          8 instances from 1 line              the count is a parameter, resolved at elaboration
the commented instance     regex may find it, design has none   comments are not part of the design
the `ifdef instance        present or absent per define         the preprocessor runs before elaboration
the parameter override     top #(8) could be #(64)              the instantiation decides, not the module

reasons: 4
reasons a better regex could fix: 0

No better regex fixes any of them, because the problem is not the matching. A parameterised, conditionally-compiled hierarchy does not have a determinate instance list until a preprocessor and an elaborator have run — and running them is what the tool does.

4. Three Kinds of Inside

The annotation's three inside items are inside for related but distinct reasons, and the distinction matters when a fourth item arrives:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
INSIDE = [
    ("get_cells", "reads the database",
     "the answer is computed from the elaborated hierarchy"),
    ("report_timing", "computes over the database",
     "needs the timing graph, the libraries and the constraints"),
    ("set_clock_groups", "mutates tool state",
     "changes how every later command behaves"),
]

print(f"{'command':<20} {'kind':<26} why it cannot leave")
for cmd, kind, why in INSIDE:
    print(f"{cmd:<20} {kind:<26} {why}")

print()
print("kinds:", len({k for _, k, _ in INSIDE}))
print("the mutating one is special because:",
      "its effect is invisible in any file")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
command              kind                       why it cannot leave
get_cells            reads the database         the answer is computed from the elaborated hierarchy
report_timing        computes over the database needs the timing graph, the libraries and the constraints
set_clock_groups     mutates tool state         changes how every later command behaves

kinds: 3
the mutating one is special because: its effect is invisible in any file

The third is the one that bites a flow designed from outside. set_clock_groups leaves no file behind: its entire effect is on the tool's in-memory state, so two sessions that ran the same commands in a different order are different sessions, and nothing Python can read afterwards distinguishes them.

That has a direct consequence for Chapter 32.4's generator. A generated script is a sequence, and for mutating commands the sequence is semantic — so the generator must not, for example, sort its output for deterministic diffs in a way that reorders constraint commands. Chapter 31.7 Section 6's rule stands: make the order a stated function of the spec, and for tool commands the stated function is "spec order", exactly as Chapter 31.2 Section 4 concluded for filelists.

5. What Cannot Usefully Happen Inside

The purpose's line has a second side, and it is the one most treatments skip:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
OUTSIDE = [
    ("decide which corners to run",
     "reads a spec file", "the tool has no reason to read it"),
    ("compare tonight's WNS with last night's",
     "needs a previous run", "the session has one design loaded"),
    ("decide pass or fail against a policy",
     "needs a threshold and a reason", "29.2's policy, not a tool concern"),
    ("fail before launching if a library is missing",
     "a pre-flight check", "21.7 — the tool cannot check before it starts"),
    ("retry a flaky step",
     "needs to outlive the session", "a crashed session cannot retry itself"),
]

print(f"{'work':<42} {'needs':<26} why not inside")
for what, needs, why in OUTSIDE:
    print(f"{what:<42} {needs:<26} {why}")

print()
print("items:", len(OUTSIDE))
print("items that need something the session does not have:", len(OUTSIDE))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
work                                       needs                      why not inside
decide which corners to run                reads a spec file          the tool has no reason to read it
compare tonight's WNS with last night's    needs a previous run       the session has one design loaded
decide pass or fail against a policy       needs a threshold and a reason 29.2's policy, not a tool concern
fail before launching if a library is missing a pre-flight check         21.7 — the tool cannot check before it starts
retry a flaky step                         needs to outlive the session a crashed session cannot retry itself

items: 5
items that need something the session does not have: 5

The last two are the strongest, because they are not about capability at all — Tcl could do both. A pre-flight check must run before the session exists, and a retry must survive the session's death, so neither can live inside a thing that has not started or has just died.

The fourth row is worth a number, because the cost of getting it wrong is measured in hours:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
STEPS = [
    ("read libraries", 4),
    ("elaborate", 12),
    ("compile", 180),
    ("optimise", 140),
    ("report", 6),
]

total = sum(m for _, m in STEPS)
print(f"{'step':<18} {'minutes':>8}  cumulative")
cum = 0
for name, m in STEPS:
    cum += m
    print(f"{name:<18} {m:>8}  {cum}")

print()
print("total session           :", total, "minutes")
print("a missing library is found at:", STEPS[0][1], "minutes — if checked first")
print("a bad corner name is found at:", cum, "minutes — if checked in the report step")
print("cost of checking late       :", cum - STEPS[0][1], "minutes of a licence")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
step                minutes  cumulative
read libraries            4  4
elaborate                12  16
compile                 180  196
optimise                140  336
report                    6  342

total session           : 342 minutes
a missing library is found at: 4 minutes — if checked first
a bad corner name is found at: 342 minutes — if checked in the report step
cost of checking late       : 338 minutes of a licence

Three hundred and forty-two minutes, and a corner name that does not exist in the library is discoverable in Python from the spec file in milliseconds, before a licence is taken. That is Chapter 21.7's pre-flight argument with a number on it, and it is the single highest-value thing the outside layer does.

6. The Test: Name the Input

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def where(inputs):
    """Decide where work belongs by what it reads."""
    db = "design database" in inputs
    ext = any(i in inputs for i in ("spec file", "previous run", "policy",
                                    "filesystem"))
    if db and ext:
        return "SPLIT — the query inside, the decision outside"
    if db:
        return "inside the tool"
    return "outside"


CASES = [
    ("get_cells -hier *reg*", {"design database"}),
    ("report_timing", {"design database"}),
    ("set_clock_groups", {"design database"}),
    ("which corners to run", {"spec file"}),
    ("is tonight worse than last night", {"previous run"}),
    ("pass or fail", {"policy"}),
    ("report timing for every clock in the design",
     {"design database", "spec file"}),
    ("check the libraries exist", {"filesystem"}),
]

print(f"{'work':<46} where")
for what, inputs in CASES:
    print(f"{what:<46} {where(inputs)}")

print()
print("cases decided by naming the input:", len(CASES))
print("cases needing judgement           :", 0)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
work                                           where
get_cells -hier *reg*                          inside the tool
report_timing                                  inside the tool
set_clock_groups                               inside the tool
which corners to run                           outside
is tonight worse than last night               outside
pass or fail                                   outside
report timing for every clock in the design    SPLIT — the query inside, the decision outside
check the libraries exist                      outside

cases decided by naming the input: 8
cases needing judgement           : 0

Eight cases, no judgement calls. The test is one question — what does this work read? — and it reproduces the annotation's five placements plus three more, including the interesting one.

7. The Case That Splits

The seventh row is the shape most real work has, and handling it badly is how a flow ends up with logic in the wrong place:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
print("the task: report timing for every clock in the design")
print()
STEPS = [
    (1, "inside", "get_clocks", "returns the clock names from the database"),
    (2, "inside", "write them to a file", "the only way they cross — 32.2 §4"),
    (3, "outside", "read the file", "Python now has the list"),
    (4, "outside", "apply the policy", "which clocks matter, in what order"),
    (5, "outside", "generate the next script", "32.4"),
    (6, "inside", "report_timing per clock", "back in the tool"),
]

print(f"{'step':>4} {'where':<9} {'what':<28} note")
for n, where, what, note in STEPS:
    print(f"{n:>4} {where:<9} {what:<28} {note}")

crossings = sum(1 for i in range(1, len(STEPS))
                if STEPS[i][1] != STEPS[i - 1][1])
print()
print("boundary crossings:", crossings)
print("the wrong design would be:", "steps 3 and 4 inside the tool")
print("why                      :", "untestable, and 32.2 §7's criterion")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
the task: report timing for every clock in the design

step where     what                         note
   1 inside    get_clocks                   returns the clock names from the database
   2 inside    write them to a file         the only way they cross — 32.2 §4
   3 outside   read the file                Python now has the list
   4 outside   apply the policy             which clocks matter, in what order
   5 outside   generate the next script     32.4
   6 inside    report_timing per clock      back in the tool

boundary crossings: 2
the wrong design would be: steps 3 and 4 inside the tool
why                      : untestable, and 32.2 §7's criterion

Six steps and two crossings, which is the minimum for this shape: the design's own facts come out through a file, Python decides, and a second generated script goes back in.

That costs one extra tool invocation, and it is worth being explicit that the alternative — doing steps 3 and 4 in Tcl inside the first session — is cheaper in wall-clock time and worse in every other respect. It saves a launch and makes the policy untestable, unversioned by the spec, and invisible to the comparison in Chapter 29.4. A flow that runs nightly can afford a second launch; it cannot afford a policy nobody can test.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
OPTIONS = [
    ("two sessions, Python decides between",
     2, "testable, versioned, comparable", "one extra launch"),
    ("one session, Tcl decides",
     1, "faster", "policy untestable and unversioned"),
]

print(f"{'design':<38} {'launches':>9}  gains / costs")
for name, n, gain, cost in OPTIONS:
    print(f"{name:<38} {n:>9}  +{gain} / -{cost}")

print()
print("the trade is:", "one tool launch against a testable policy")
print("for a nightly flow that is:", "obviously worth it")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
design                                  launches  gains / costs
two sessions, Python decides between           2  +testable, versioned, comparable / -one extra launch
one session, Tcl decides                       1  +faster / -policy untestable and unversioned

the trade is: one tool launch against a testable policy
for a nightly flow that is: obviously worth it

8. What a Python Wrapper Should Offer Instead

Having established what not to re-implement, the useful question is what Python should provide around a tool API:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
OFFERINGS = [
    ("a typed record of a parsed report", "yes", "28.1 — the tool emits text"),
    ("a comparison between two runs", "yes", "29.4, 28.5 — the tool sees one"),
    ("a policy with a threshold and a reason", "yes", "29.2"),
    ("retry, timeout and a run directory", "yes", "25.x, 26.x"),
    ("a reimplementation of get_cells", "no", "§3"),
    ("a reimplementation of report_timing", "no", "§3"),
    ("a Python API that wraps the tool's Tcl", "sometimes",
     "useful as a thin shim; dangerous as an abstraction"),
]

print(f"{'what Python offers':<42} {'worth it':<11} reference")
for what, worth, ref in OFFERINGS:
    print(f"{what:<42} {worth:<11} {ref}")

yes = [o for o in OFFERINGS if o[1] == "yes"]
print()
print("clear yes:", len(yes))
print("clear no :", sum(1 for o in OFFERINGS if o[1] == "no"))
print("every yes has one thing in common:",
      "its input is not the design database")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
what Python offers                         worth it    reference
a typed record of a parsed report          yes         28.1 — the tool emits text
a comparison between two runs              yes         29.4, 28.5 — the tool sees one
a policy with a threshold and a reason     yes         29.2
retry, timeout and a run directory         yes         25.x, 26.x
a reimplementation of get_cells            no          §3
a reimplementation of report_timing        no          §3
a Python API that wraps the tool's Tcl     sometimes   useful as a thin shim; dangerous as an abstraction

clear yes: 4
clear no : 2
every yes has one thing in common: its input is not the design database

Every clear yes has an input that is not the design database — text the tool emitted, a previous run, a policy file, a filesystem. That is the test from Section 6, run backwards over the whole of Part II.

The last row deserves its hedge. A thin Python function that emits one Tcl command with escaped arguments is Chapter 32.4's tcl_str and is plainly good. A Python class hierarchy modelling cells and nets, backed by generated Tcl queries, is the thing that becomes a second tool API — and it is attractive precisely because the first few methods are easy.

9. Testing Generated Tcl Without the Tool

Section 5 said a pre-flight check must run before the session exists. That raises a question the chapter should answer: if the tool's commands can only run inside the tool, how is a generated script tested at all?

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
LEVELS = [
    ("does it parse as Tcl?", "tclsh, no tool", "yes — §9"),
    ("are the values escaped correctly?", "tclsh, no tool", "yes — 32.4 §5"),
    ("is the command sequence the one the spec implies?",
     "a structural check", "yes"),
    ("does set_clock_groups accept these arguments?", "needs the tool", "no"),
    ("does report_timing find the path?", "needs the design", "no"),
]

print(f"{'question':<48} {'needs':<20} testable here")
for q, needs, ok in LEVELS:
    print(f"{q:<48} {needs:<20} {ok}")

print()
print("questions answerable without a licence:", 3)
print("questions needing the tool            :", 2)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
question                                         needs                testable here
does it parse as Tcl?                            tclsh, no tool       yes — §9
are the values escaped correctly?                tclsh, no tool       yes — 32.4 §5
is the command sequence the one the spec implies? a structural check   yes
does set_clock_groups accept these arguments?    needs the tool       no
does report_timing find the path?                needs the design     no

questions answerable without a licence: 3
questions needing the tool            : 2

Three of five questions need no licence, which is more than most teams assume — and the first of them is answerable with a trick worth knowing:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import subprocess
from pathlib import Path

GENERATED = """# GENERATED by gen_sta.py — do not edit
read_verilog /proj/rtl/top.v
create_clock -name clk -period 2.0 [get_ports clk]
set_clock_groups -asynchronous -group {clk} -group {jtag_tck}
report_timing -delay_type max -path_type full
"""
Path("run.tcl").write_text(GENERATED, encoding="utf-8")

STUBS = """proc read_verilog args { puts "STUB read_verilog $args" }
proc create_clock args { puts "STUB create_clock $args" }
proc get_ports args { return [list {*}$args] }
proc set_clock_groups args { puts "STUB set_clock_groups $args" }
proc report_timing args { puts "STUB report_timing $args" }
source run.tcl
"""
Path("harness.tcl").write_text(STUBS, encoding="utf-8")

r = subprocess.run(["tclsh", "harness.tcl"], stdout=subprocess.PIPE,
                   stderr=subprocess.STDOUT, text=True,
                   stdin=subprocess.DEVNULL)
print("returncode:", r.returncode)
for line in r.stdout.strip().splitlines():
    print("  ", line)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
returncode: 0
   STUB read_verilog /proj/rtl/top.v
   STUB create_clock -name clk -period 2.0 clk
   STUB set_clock_groups -asynchronous -group clk -group jtag_tck
   STUB report_timing -delay_type max -path_type full

Five tool commands exercised with no tool. Defining a proc for each command turns tclsh into a harness that proves the script parses, that every command is reached, that the arguments are the ones intended, and that the order is right.

What it does not prove is that the real commands accept those arguments — a stub accepts anything. So this is a structural test rather than a semantic one. That is the trade Chapter 23.1 Section 1's standing constraint forces throughout Part II: when the real executor is unavailable, a check of the artefact's structure is worth having precisely because it is honest about what it does not cover.

The valuable part is what the stubs make visible:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import re
import subprocess
from pathlib import Path

GENERATED = """read_verilog /proj/rtl/top.v
create_clock -name clk -period 2.0 [get_ports clk]
set_clock_groups -asynchronous -group {clk} -group {jtag_tck}
report_timing -delay_type max
"""
Path("run.tcl").write_text(GENERATED, encoding="utf-8")
Path("harness.tcl").write_text(
    "set ::calls [list]\n"
    "foreach c {read_verilog create_clock set_clock_groups report_timing} {\n"
    "  proc $c args \"lappend ::calls \\[list $c {*}\\$args\\]\"\n"
    "}\n"
    "proc get_ports args { return [list {*}$args] }\n"
    "source run.tcl\n"
    "foreach call $::calls { puts [lindex $call 0] }\n",
    encoding="utf-8")

r = subprocess.run(["tclsh", "harness.tcl"], stdout=subprocess.PIPE,
                   stderr=subprocess.STDOUT, text=True,
                   stdin=subprocess.DEVNULL)
order = r.stdout.strip().splitlines()
print("commands, in the order executed:")
for c in order:
    print("  ", c)

EXPECTED = ["read_verilog", "create_clock", "set_clock_groups", "report_timing"]
print()
print("expected order:", EXPECTED == order)
print("constraints before reporting:",
      order.index("create_clock") < order.index("report_timing"))
print("clock groups before reporting:",
      order.index("set_clock_groups") < order.index("report_timing"))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
commands, in the order executed:
   read_verilog
   create_clock
   set_clock_groups
   report_timing

expected order: True
constraints before reporting: True
clock groups before reporting: True

Three ordering properties checked without a licence, and the second and third are the ones that matter: Section 4 established that set_clock_groups mutates tool state, so a generated script that reports before setting its clock groups produces real numbers for the wrong configuration — and nothing downstream would notice, because the report looks entirely normal.

That is a defect a structural test catches and a tool run would not, which is an unusual and useful inversion: the licence-free check finds the ordering bug, and the tool finds the argument bug.

10. Predict, Run, Observe

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import re
import subprocess
from pathlib import Path

SRC = """module leaf(input a, output b); assign b = ~a; endmodule
module top #(parameter N = 4) (input [3:0] a, output [3:0] b);
  genvar i;
  generate for (i = 0; i < N; i = i + 1) begin : bk
    leaf u (.a(a[i]), .b(b[i]));
  end endgenerate
endmodule
module tb; reg [3:0] a = 0; wire [3:0] b; top #(4) t(.a(a), .b(b));
  initial begin #1 $display("ok"); $finish; end endmodule
"""
Path("p.v").write_text(SRC, encoding="utf-8")

# A
print("A:", len(re.findall(r"^\s*leaf\s+\w+\s*\(", SRC, re.M)))

# B
print("B:", SRC.count("genvar"), "genvar,", SRC.count("N = 4"), "parameter default")

# C
c = subprocess.run(["iverilog", "-g2005", "-o", "p.vvp", "p.v"],
                   stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
print("C:", c.returncode)

# D
print("D:", "design database" in {"design database"},
      "design database" in {"spec file"})

The verified output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A: 1
B: 1 genvar, 1 parameter default
C: 0
D: True False

A — one leaf instantiation in the text, which will elaborate to four.

B — the two features that make the text and the hierarchy disagree, both present in six lines of ordinary Verilog.

C — the elaborator accepts it, and only the elaborator knows the answer is four.

D — the test from Section 6, as two membership checks.

11. Exercises

Exercise 1 — the temptation. Write down the strongest argument you have heard for re-implementing a tool query in Python, and answer it.

Exercise 2 — strings versus data. Explain in one sentence why Python's better string handling is irrelevant to this question.

Exercise 3 — the annotation's sixth item. Pick a tool command not in the annotation and place it, stating your reason.

Exercise 4 — three kinds of inside. Classify five tool commands as reading, computing or mutating.

Exercise 5 — regex against elaboration. Reproduce Section 3 on a module of your own and report both counts.

Exercise 6 — the generate loop. Find a generate loop in your RTL and say how many instances one line produces.

Exercise 7 — the define that changes the design. Find an `ifdef in your RTL that adds or removes an instance.

Exercise 8 — the parameter override. Show that the module's default does not determine the count.

Exercise 9 — no better regex. Explain why improving the pattern cannot close the gap.

Exercise 10 — effort versus information. Restate the limit in terms of what the input contains.

Exercise 11 — a second elaborator. Describe what a Python program would have to do to answer get_cells correctly.

Exercise 12 — mutating state leaves no file. Give an example from your flow where command order changed the result.

Exercise 13 — do not sort tool commands. Say what breaks if a generator sorts constraint commands for a clean diff.

Exercise 14 — the outside list. Add two items of your own to Section 5's list, with reasons.

Exercise 15 — before the session exists. Name a check your flow does late that must happen before launch.

Exercise 16 — surviving the session's death. Say what in your flow has to outlive a crashed tool.

Exercise 17 — the 342 minutes. Put real numbers on your own longest session and compute the cost of checking late.

Exercise 18 — name the input. Apply Section 6's test to eight pieces of your own flow.

Exercise 19 — a case that splits. Find work in your flow that needs both the database and a policy.

Exercise 20 — count the crossings. For that work, say how many boundary crossings the correct design needs.

Exercise 21 — the cheaper, worse option. Price one extra tool launch against an untestable policy for your flow.

Exercise 22 — what Python should offer. List what your Python layer provides and check each input against the test.

Exercise 23 — the thin shim. Decide where your wrapper stops being a shim and starts being an abstraction.

Exercise 24 — the first few methods are easy. Explain why that is the warning sign rather than the encouragement.

12. Summary

  • Python is better at strings than Tcl and that is irrelevant. The question is which process holds the data.
  • The annotation places five items correctly and gives no reason, so a reader cannot place a sixth — the same gap Chapter 31.1 filled with a metric.
  • A regex get_cells found 1 instance where the elaborator created 8, and 9 under a define that left the regex's answer unchanged.
  • Four ordinary features make the text and the hierarchy disagree — a generate loop, a comment, an `ifdef, and a parameter override at the instantiation — and no better regex fixes any of them.
  • The limit is information, not effort. To answer the query you must elaborate, and a program that elaborates is a second, less-tested tool.
  • Three kinds of work are inside: reading the database, computing over it, and mutating tool state.
  • A mutating command leaves no file behind, so command order is semantic and a generator must not reorder tool commands for a tidy diff.
  • Two kinds of work cannot be inside regardless of capability: a pre-flight check must run before the session exists, and a retry must survive its death.
  • A 342-minute session can discover a bad corner name in milliseconds beforehand, which is the highest-value thing the outside layer does.
  • The test is one question — what does this work read? Design database means inside; a spec file, a previous run, a policy or the filesystem means outside; both means the work splits.
  • A split task needs two crossings and one extra tool launch, and the cheaper single-session alternative buys speed with an untestable policy.
  • Every worthwhile thing Python offers around a tool has an input that is not the design database.
  • A thin Tcl-emitting shim is good and a class hierarchy modelling cells is a second tool API — attractive because the first few methods are easy.
  • Three of five questions about a generated script need no licence: does it parse, are the values escaped, and is the command sequence right.
  • Defining a proc per tool command turns tclsh into a harness — five tool commands exercised with no tool, proving the script parses, every command is reached, and the arguments are the intended ones.
  • A stub accepts anything, so this is a structural test — honest about what it does not cover, which is the trade Chapter 23.1 Section 1's constraint forces throughout Part II.
  • The licence-free check finds the ordering bug and the tool finds the argument bug — and an ordering bug matters because set_clock_groups mutates state, so reporting before setting it gives real numbers for the wrong configuration.

The next chapter takes the case where there is no Tcl at all and Python is the whole flow.

Where this fits

Part of the Python curriculum.