Skip to content
VLSI Mentor

Python Part II · Module 31 · Generating RTL and Verification Artifacts

Generating Repetitive RTL Structure

This is the generator the module is named after and the one with the simplest shape: one line, repeated. It is also the chapter where the module argues against itself, because a hardware description language already has a loop — and for the textbook example of a sixty-four-way comparator bank, the HDL's own version is one line to Python's sixty-four and is provably the same circuit. Working out exactly when Python is still the right answer turns out to be the useful part, and the annotation's line does not compile, which is a good place to start.

Advanced21 min readPythonPart IIAutomationCode GenerationRTL

Part II · Module 31 · Chapter 31.4 · Generating Repetitive RTL Structure

1. One Line, Repeated

By Chapter 31.1's metric this is the best possible generator target:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import re

N = 8
TAGS = [0x10, 0x21, 0x32, 0x43, 0x54, 0x65, 0x76, 0x87]

lines = [f"  assign hit[{i}] = (tag == 8'h{t:02X});" for i, t in enumerate(TAGS)]
for l in lines:
    print(l)

shape = lambda s: re.sub(r"\d+", "#", s.strip())
print()
print("lines :", len(lines))
print("shapes:", len({shape(l) for l in lines}))
print("review discount:", f"{len({shape(l) for l in lines}) / len(lines):.3f}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  assign hit[0] = (tag == 8'h10);
  assign hit[1] = (tag == 8'h21);
  assign hit[2] = (tag == 8'h32);
  assign hit[3] = (tag == 8'h43);
  assign hit[4] = (tag == 8'h54);
  assign hit[5] = (tag == 8'h65);
  assign hit[6] = (tag == 8'h76);
  assign hit[7] = (tag == 8'h87);

lines : 8
shapes: 1
review discount: 0.125

One shape, eight lines, and at sixty-four the discount is 0.016. There is nothing to argue about: a reviewer reads one line and the loop.

So the rest of the chapter is about two things the metric does not cover — whether the emitted line is even valid, and whether Python should have been involved at all.

2. The Annotation Does Not Compile

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
for i in range(n):
    print(f"  assign hit[{i}] = (tag == tags[{i}]);", file=f)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import subprocess
from pathlib import Path

N = 8
body = "\n".join(f"  assign hit[{i}] = (tag == tags[{i}]);" for i in range(N))
Path("bank.v").write_text(
    f"module bank(input [7:0] tag, output [{N-1}:0] hit);\n{body}\nendmodule\n",
    encoding="utf-8")

print("emitted first line:", body.splitlines()[0].strip())
print()
c = subprocess.run(["iverilog", "-g2005", "-o", "bank.vvp", "bank.v"],
                   stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
print("returncode:", c.returncode)
print("first error:", c.stdout.strip().splitlines()[0])
print("error lines:", len([l for l in c.stdout.splitlines() if "error" in l]))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
emitted first line: assign hit[0] = (tag == tags[0]);

returncode: 16
first error: bank.v:2: error: Unable to bind wire/reg/memory `tags['sd0]' in `bank'
error lines: 17

tags does not exist. The annotation writes an index into an RTL array that nothing declared, and every one of the eight lines fails to bind.

That is not a typo in the annotation — it is an ambiguity, and it is worth naming because the two readings need different code:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
READINGS = [
    ("the tags are data in my spec",
     "emit the literal value", "8'h32", "§3"),
    ("the tags are a signal array in the design",
     "declare the array and index it", "tags[8*i +: 8]", "§4"),
]

print(f"{'what the author meant':<36} {'what to emit':<32} {'example':<16} §")
for meant, emit, ex, sec in READINGS:
    print(f"{meant:<36} {emit:<32} {ex:<16} {sec}")

print()
print("readings:", len(READINGS))
print("the annotation's line is correct for:", 0)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
what the author meant                what to emit                     example          §
the tags are data in my spec         emit the literal value           8'h32            §3
the tags are a signal array in the design declare the array and index it   tags[8*i +: 8]   §4

readings: 2
the annotation's line is correct for: 0

3. If the Values Are Data, Emit the Values

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

TAGS = [0x10, 0x21, 0x32, 0x43, 0x54, 0x65, 0x76, 0x87]
N = len(TAGS)

body = "\n".join(f"  assign hit[{i}] = (tag == 8'h{t:02X});"
                 for i, t in enumerate(TAGS))
Path("bank.v").write_text(
    "// GENERATED by gen_bank.py from tags.csv — do not edit\n"
    f"module bank(input [7:0] tag, output [{N-1}:0] hit);\n{body}\nendmodule\n",
    encoding="utf-8")

Path("tb.v").write_text("""module tb;
  reg [7:0] tag;
  wire [7:0] hit;
  bank u(.tag(tag), .hit(hit));
  integer k, lit, multi;
  initial begin
    lit = 0; multi = 0;
    for (k = 0; k < 256; k = k + 1) begin
      tag = k[7:0]; #1;
      if (hit != 0) lit = lit + 1;
      if (hit != 0 && (hit & (hit - 1)) != 0) multi = multi + 1;
    end
    $display("tags that matched something: %0d", lit);
    $display("tags that matched more than one: %0d", multi);
    tag = 8'h32; #1; $display("tag=32 hit=%b", hit);
    $finish;
  end
endmodule
""", encoding="utf-8")

c = subprocess.run(["iverilog", "-g2005", "-o", "b.vvp", "bank.v", "tb.v"],
                   stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
print("compile returncode:", c.returncode)
r = subprocess.run(["vvp", "b.vvp"], stdout=subprocess.PIPE,
                   stderr=subprocess.STDOUT, text=True)
for l in r.stdout.splitlines():
    if not l.startswith("tb.v"):
        print("  ", l)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
compile returncode: 0
   tags that matched something: 8
   tags that matched more than one: 0
   tag=32 hit=00000100

Compiled, and exercised over the whole input space: eight of the 256 tag values match, none matches two comparators, and 0x32 lights bit 2 — which is its index in the spec.

That exhaustive sweep is cheap here and is the right habit for a generated bank, because it checks the property the generator is responsible for: the mapping from spec order to bit position. A generator that reversed the list would still compile and still produce one hit per tag; only the bit index would be wrong, and only this check would see it.

4. If the Values Are a Signal, Declare the Array

The other reading needs the array to exist, and a byte-slice of a flattened bus:

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

N = 8
Path("bank_sig.v").write_text(f"""// GENERATED by gen_bank.py — do not edit
module bank_sig(
  input  [7:0]        tag,
  input  [{8*N-1}:0]  tags,
  output [{N-1}:0]    hit
);
""" + "\n".join(f"  assign hit[{i}] = (tag == tags[{8*i+7}:{8*i}]);"
                for i in range(N)) + "\nendmodule\n", encoding="utf-8")

print(Path("bank_sig.v").read_text(encoding="utf-8"), end="")
c = subprocess.run(["iverilog", "-g2005", "-o", "s.vvp", "bank_sig.v"],
                   stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
print()
print("compile returncode:", c.returncode)
print("note the emitted slice bounds are computed in Python:", "8*i+7 : 8*i")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// GENERATED by gen_bank.py — do not edit
module bank_sig(
  input  [7:0]        tag,
  input  [63:0]  tags,
  output [7:0]    hit
);
  assign hit[0] = (tag == tags[7:0]);
  assign hit[1] = (tag == tags[15:8]);
  assign hit[2] = (tag == tags[23:16]);
  assign hit[3] = (tag == tags[31:24]);
  assign hit[4] = (tag == tags[39:32]);
  assign hit[5] = (tag == tags[47:40]);
  assign hit[6] = (tag == tags[55:48]);
  assign hit[7] = (tag == tags[63:56]);
endmodule

compile returncode: 0
note the emitted slice bounds are computed in Python: 8*i+7 : 8*i

This compiles, and it is the version that makes Section 5's argument unavoidable: every one of those eight lines was produced by arithmetic that Verilog can do itself.

5. The HDL Already Has a Loop

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

N = 8

Path("bank_py.v").write_text(
    f"module bank_py(input [7:0] tag, input [{8*N-1}:0] tags, "
    f"output [{N-1}:0] hit);\n"
    + "\n".join(f"  assign hit[{i}] = (tag == tags[{8*i+7}:{8*i}]);"
                for i in range(N))
    + "\nendmodule\n", encoding="utf-8")

Path("bank_gen.v").write_text("""module bank_gen #(parameter N = 8) (
  input  [7:0]     tag,
  input  [8*N-1:0] tags,
  output [N-1:0]   hit
);
  genvar i;
  generate
    for (i = 0; i < N; i = i + 1) begin : cmp
      assign hit[i] = (tag == tags[8*i +: 8]);
    end
  endgenerate
endmodule
""", encoding="utf-8")

Path("tb.v").write_text("""module tb;
  reg [7:0] tag;
  wire [7:0] hit_py, hit_gen;
  wire [63:0] tags = {8'h87,8'h76,8'h65,8'h54,8'h43,8'h32,8'h21,8'h10};
  bank_py  a(.tag(tag), .tags(tags), .hit(hit_py));
  bank_gen #(8) b(.tag(tag), .tags(tags), .hit(hit_gen));
  integer k, diff;
  initial begin
    diff = 0;
    for (k = 0; k < 256; k = k + 1) begin
      tag = k[7:0]; #1;
      if (hit_py !== hit_gen) diff = diff + 1;
    end
    $display("tag values compared: 256");
    $display("disagreements      : %0d", diff);
    $finish;
  end
endmodule
""", encoding="utf-8")

c = subprocess.run(["iverilog", "-g2005", "-o", "x.vvp",
                    "bank_py.v", "bank_gen.v", "tb.v"],
                   stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
print("compile returncode:", c.returncode)
r = subprocess.run(["vvp", "x.vvp"], stdout=subprocess.PIPE,
                   stderr=subprocess.STDOUT, text=True)
for l in r.stdout.splitlines():
    if not l.startswith("tb.v"):
        print("  ", l)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
compile returncode: 0
   tag values compared: 256
   disagreements      : 0

Zero disagreements over the entire input space. The two modules are the same circuit, established by exhaustion rather than by claiming it.

Now price them:

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

shape = lambda s: re.sub(r"\d+", "#", s.strip())

PY = [f"  assign hit[{i}] = (tag == tags[{8*i+7}:{8*i}]);" for i in range(8)]
GEN = ["      assign hit[i] = (tag == tags[8*i +: 8]);"]

for label, lines, extra in (("python-generated", PY, "+ a generator script"),
                            ("verilog generate-for", GEN, "+ nothing")):
    print(f"{label:<22} assign lines={len(lines):<2} "
          f"shapes={len({shape(l) for l in lines})}  {extra}")

print()
print("same shape count       :", True)
print("lines in the source    :", f"{len(PY)} vs {len(GEN)}")
print("files a reviewer opens :", f"2 vs 1")
print("changes when N becomes 64:", "regenerate and re-review vs edit one parameter")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
python-generated       assign lines=8  shapes=1  + a generator script
verilog generate-for   assign lines=1  shapes=1  + nothing

same shape count       : True
lines in the source    : 8 vs 1
files a reviewer opens : 2 vs 1
changes when N becomes 64: regenerate and re-review vs edit one parameter

Identical shape counts, and the HDL version is one line, needs no second language, no build step, and no do-not-edit discipline. Changing eight to sixty-four is editing a parameter rather than rerunning a generator and reviewing a new file.

So the module's own tool loses this comparison, and the rule that follows is the one worth taking away:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
CASES = [
    ("64 comparators against a signal array", "N is a parameter",
     "generate for", "no"),
    ("64 comparators against spec tag values", "values come from tags.csv",
     "python", "yes"),
    ("per-channel instances, identical", "N is a parameter",
     "generate for", "no"),
    ("per-channel instances, per-channel config", "config comes from a spec",
     "python", "yes"),
    ("a register block", "names and offsets come from regs.csv",
     "python", "yes"),
    ("an IP wrapper", "ports come from the IP's source",
     "python", "yes"),
]

print(f"{'what':<42} {'where the data lives':<34} {'use':<13} python?")
for what, where, use, py in CASES:
    print(f"{what:<42} {where:<34} {use:<13} {py}")

print()
need = [c for c in CASES if c[3] == "yes"]
print("cases needing python:", len(need))
print("what they have in common:", "the data is outside the HDL")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
what                                       where the data lives               use           python?
64 comparators against a signal array      N is a parameter                   generate for  no
64 comparators against spec tag values     values come from tags.csv          python        yes
per-channel instances, identical           N is a parameter                   generate for  no
per-channel instances, per-channel config  config comes from a spec           python        yes
a register block                           names and offsets come from regs.csv python        yes
an IP wrapper                              ports come from the IP's source    python        yes

cases needing python: 4
what they have in common: the data is outside the HDL

Generate only what the HDL cannot express, and what it cannot express is data that lives outside it. Every chapter in this module that survives that test does so for the same reason: Chapter 31.2's filelist comes from a spec, Chapter 31.3's offsets come from a CSV, Chapter 31.5's ports come from somebody else's source file.

6. The Returncode Is an Error Count

Two of this chapter's failures produced returncodes worth recording, because they break the obvious wrapper:

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

TAGS = [0x10, 0x21, 0x32, 0x43, 0x54, 0x65, 0x76, 0x87]

CASES = {}
CASES["8 undeclared references"] = (
    "module u(input [7:0] tag, output [7:0] hit);\n"
    + "\n".join(f"  assign hit[{i}] = (tag == tags[{i}]);" for i in range(8))
    + "\nendmodule\n")
CASES["4 out-of-range indices"] = (
    "module n(input [7:0] tag, output [3:0] hit);\n"
    + "\n".join(f"  assign hit[{i}] = (tag == 8'h{TAGS[i]:02X});" for i in range(8))
    + "\nendmodule\n")
CASES["1 syntax error"] = "module s(input a; output b); endmodule\n"
CASES["clean"] = (
    "module c(input [7:0] tag, output [7:0] hit);\n"
    + "\n".join(f"  assign hit[{i}] = (tag == 8'h{TAGS[i]:02X});" for i in range(8))
    + "\nendmodule\n")

print(f"{'case':<26} {'rc':>4}  first message")
for label, src in CASES.items():
    Path("t.v").write_text(src, encoding="utf-8")
    c = subprocess.run(["iverilog", "-g2005", "-o", "t.vvp", "t.v"],
                       stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
    first = c.stdout.strip().splitlines()[0][:44] if c.stdout.strip() else "(none)"
    print(f"{label:<26} {c.returncode:>4}  {first}")

print()
print("is there a fixed code for a syntax error?", False)
print("rc is:", "the number of errors the compiler counted")
print("so a wrapper must test:", "rc != 0")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
case                         rc  first message
8 undeclared references      16  t.v:2: error: Unable to bind wire/reg/memory
4 out-of-range indices        4  t.v:6: error: Index hit[4] is out of range.
1 syntax error                3  t.v:1: error: ';' is an invalid port declara
clean                         0  (none)

is there a fixed code for a syntax error? False
rc is: the number of errors the compiler counted
so a wrapper must test: rc != 0

Sixteen, four, three, zero. The returncode varies with how many problems the compiler reported and not with their kind: eight undeclared references give 16 (each is reported twice), four out-of-range indices give 4, and a single malformed port declaration gives 3 because it cascades into three messages.

The useful conclusion is the negative one. There is no fixed code meaning syntax error and no fixed code meaning bind failure, so the number carries no classification at all.

A wrapper that branches on rc == 1 or rc == 2 will mis-handle most real failures, which is Chapter 22.4's argument about exit codes arriving in a specific tool. The only portable test is rc != 0, and the kind of failure has to come from the message, not the number.

The second row is also a defect worth naming on its own: hit[3:0] with eight assignments is a generator whose two size inputs disagreed — the port width came from one place and the loop bound from another. One variable, used twice, removes it.

7. The Generator, Assembled

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import re
import subprocess
from dataclasses import dataclass
from pathlib import Path
from typing import List

HDR = "// GENERATED by gen_bank.py from tags.csv — do not edit"


@dataclass(frozen=True)
class Bank:
    name: str
    tag_bits: int
    tags: List[int]

    @property
    def n(self):
        return len(self.tags)

    def problems(self):
        out = []
        limit = 1 << self.tag_bits
        too_big = [f"0x{t:X}" for t in self.tags if t >= limit]
        if too_big:
            out.append(f"tag exceeds {self.tag_bits} bits: {too_big}")
        dupes = sorted({f"0x{t:X}" for t in self.tags
                        if self.tags.count(t) > 1})
        if dupes:
            out.append(f"duplicate tags (two hits at once): {dupes}")
        if not self.tags:
            out.append("empty tag list")
        return out

    def emit(self):
        n, w = self.n, self.tag_bits
        L = [HDR,
             f"module {self.name}(",
             f"  input  [{w-1}:0] tag,",
             f"  output [{n-1}:0] hit",
             ");"]
        for i, t in enumerate(self.tags):
            L.append(f"  assign hit[{i}] = (tag == {w}'h{t:0{(w+3)//4}X});")
        L.append("endmodule")
        return "\n".join(L) + "\n"


good = Bank("tagbank", 8, [0x10, 0x21, 0x32, 0x43, 0x54, 0x65, 0x76, 0x87])
bad = Bank("tagbank", 8, [0x10, 0x21, 0x21, 0x100])

print("good spec problems:", good.problems() or "none")
print("bad  spec problems:")
for p in bad.problems():
    print("   -", p)
print()

rtl = good.emit()
Path("tagbank.v").write_text(rtl, encoding="utf-8")
print("generated lines:", len(rtl.splitlines()))
print("header present :", rtl.splitlines()[0].startswith("// GENERATED"))
shape = lambda s: re.sub(r"\d+", "#", s.strip())
assigns = [l for l in rtl.splitlines() if "assign" in l]
print("assign lines   :", len(assigns))
print("distinct shapes:", len({shape(l) for l in assigns}))
print()

Path("tb.v").write_text("""module tb;
  reg [7:0] tag;
  wire [7:0] hit;
  tagbank u(.tag(tag), .hit(hit));
  integer k, hits, multi;
  initial begin
    hits = 0; multi = 0;
    for (k = 0; k < 256; k = k + 1) begin
      tag = k[7:0]; #1;
      if (hit != 0) hits = hits + 1;
      if (hit != 0 && (hit & (hit - 1)) != 0) multi = multi + 1;
    end
    $display("matched: %0d  multi-hit: %0d", hits, multi);
    $finish;
  end
endmodule
""", encoding="utf-8")

c = subprocess.run(["iverilog", "-g2005", "-o", "t.vvp", "tagbank.v", "tb.v"],
                   stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
print("compile rc:", c.returncode)
r = subprocess.run(["vvp", "t.vvp"], stdout=subprocess.PIPE,
                   stderr=subprocess.STDOUT, text=True)
line = [l for l in r.stdout.splitlines() if l.startswith("matched")][0]
print("simulation:", line)
print()
print("tags in the spec      :", good.n)
print("one hit per spec tag  :", line.split()[1] == str(good.n))
print("never two hits at once:", line.split()[-1] == "0")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
good spec problems: none
bad  spec problems:
   - tag exceeds 8 bits: ['0x100']
   - duplicate tags (two hits at once): ['0x21']

generated lines: 14
header present : True
assign lines   : 8
distinct shapes: 1

compile rc: 0
simulation: matched: 8  multi-hit: 0

tags in the spec      : 8
one hit per spec tag  : True
never two hits at once: True

The validation mirrors Chapter 31.3's: a tag too wide for the bus can never match, and a duplicated tag produces two simultaneous hits, which is a hardware behaviour the downstream logic almost certainly does not expect. Both are caught before emitting.

Note the last two checks are over the simulation, not the string: eight spec tags produce eight matching addresses and never a double hit. That is the generator's contract expressed as a property of the circuit.

8. N = 1 and N = 0

A generator is written and tested at the size that motivated it. The two sizes nobody tries are the ones that break it:

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


def emit(n, tags):
    return (f"module bank(input [7:0] tag, output [{n-1}:0] hit);\n"
            + "\n".join(f"  assign hit[{i}] = (tag == 8'h{tags[i]:02X});"
                        for i in range(n))
            + "\nendmodule\n")


TAGS = [0x10, 0x21, 0x32, 0x43]

for n in (4, 1, 0):
    src = emit(n, TAGS[:n])
    Path("b.v").write_text(src, encoding="utf-8")
    c = subprocess.run(["iverilog", "-g2005", "-o", "b.vvp", "b.v"],
                       stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
    port = src.splitlines()[0].split("output")[1].split("hit")[0].strip()
    msg = c.stdout.strip().splitlines()[0][:40] if c.stdout.strip() else "(clean)"
    print(f"N={n}  port width {port:<8} rc={c.returncode}  {msg}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
N=4  port width [3:0]    rc=0  (clean)
N=1  port width [0:0]    rc=0  (clean)
N=0  port width [-1:0]   rc=0  (clean)

N=1 gives [0:0], which is a legal one-bit vector and compiles — mildly unusual-looking and entirely correct.

N=0 is the interesting one: it emits output [-1:0] hit with no assignments at all, and the compiler accepts it. A reversed, negative-width port declaration is not an error in Verilog, so a generator handed an empty spec produces a module that compiles, has a port nothing can drive, and does nothing.

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

Path("b0.v").write_text(
    "module bank(input [7:0] tag, output [-1:0] hit);\nendmodule\n",
    encoding="utf-8")
Path("tb0.v").write_text("""module tb;
  reg [7:0] tag = 8'h10;
  wire [0:0] hit;
  bank u(.tag(tag), .hit(hit));
  initial begin
    #1 $display("hit=%b  width=%0d", hit, $bits(hit));
    $display("the module compiled and connected");
    $finish;
  end
endmodule
""", encoding="utf-8")

c = subprocess.run(["iverilog", "-g2005", "-o", "z.vvp", "b0.v", "tb0.v"],
                   stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
print("compile returncode:", c.returncode)
r = subprocess.run(["vvp", "z.vvp"], stdout=subprocess.PIPE,
                   stderr=subprocess.STDOUT, text=True)
for l in r.stdout.splitlines():
    if not l.startswith("tb0.v"):
        print("  ", l)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
compile returncode: 0
   hit=z  width=1
   the module compiled and connected

An empty spec produced a module that instantiates, connects and drives nothing, and its output reads as z — high impedance, because the wire has no driver at all rather than a conflicting one. Downstream logic sees an undriven net, which propagates z into whatever reads it and becomes a dangling net in synthesis; neither symptom points back at an empty CSV.

The fix is a guard, and the useful part is choosing which sizes are legal rather than only rejecting zero:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def check_size(n, minimum=1, maximum=1024):
    if n < minimum:
        return f"refusing N={n}: the spec is empty or the count is wrong"
    if n > maximum:
        return (f"refusing N={n}: above the reviewed maximum of {maximum} — "
                "raise it deliberately")
    return None


for n in (0, 1, 4, 64, 4096):
    msg = check_size(n)
    print(f"N={n:<6} {'ok' if msg is None else msg}")

print()
print("sizes a generator should be tested at:", [0, 1, 2, "N", "N_max", "N_max + 1"])
print("sizes a generator is usually tested at:", ["N"])
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
N=0      refusing N=0: the spec is empty or the count is wrong
N=1      ok
N=4      ok
N=64     ok
N=4096   refusing N=4096: above the reviewed maximum of 1024 — raise it deliberately

sizes a generator should be tested at: [0, 1, 2, 'N', 'N_max', 'N_max + 1']
sizes a generator is usually tested at: ['N']

An upper bound looks like bureaucracy and is not: a generator that happily emits four thousand comparators from a spec with a stray row will produce a file that compiles, synthesises into something enormous, and is discovered at the area review. The maximum is a statement that somebody has thought about the size, and raising it is a one-line change with a reviewer attached — which is the same argument Chapter 29.2 Section 7 made about thresholds carrying reasons.

The last line of that output is the honest summary of how generators are tested in practice, and it is why N=0 and N=1 are worth a section of their own.

9. Predict, Run, Observe

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import re

shape = lambda s: re.sub(r"\d+", "#", s.strip())

# A
print("A:", len({shape(f"  assign hit[{i}] = (tag == 8'h{i:02X});")
                 for i in range(8)}))

# B
print("B:", len({shape("  assign hit[i] = (tag == tags[8*i +: 8]);")}))

# C
print("C:", f"tags[{8*2+7}:{8*2}]", "vs", "tags[8*i +: 8]")

# D
print("D:", 16 != 0, 16 == 2)

The verified output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A: 1
B: 1
C: tags[23:16] vs tags[8*i +: 8]
D: True False

A — eight generated lines, one shape.

B — the generate for body, also one shape. The metric cannot tell them apart, which is why Section 5 needed the line count as well.

C — the same slice, computed in Python and in Verilog. The second needs no second language.

D — rc != 0 is true and rc == 2 is false for a real failure. Test the first.

10. Exercises

Exercise 1 — your discount. Compute lines, shapes and the ratio for one repetitive block in your project.

Exercise 2 — emit the annotation. Write the annotation's line verbatim into a module and compile it. Report the returncode and the error count.

Exercise 3 — two readings. State which of the two readings your own generator meant, and whether its output matches.

Exercise 4 — literals from a spec. Generate a comparator bank from a CSV of values and compile it.

Exercise 5 — sweep the input space. Exercise your bank over every input value and count the matches.

Exercise 6 — reverse the list. Reverse your spec order and say which check catches it.

Exercise 7 — the signal-array version. Generate the flattened-bus version and compile it.

Exercise 8 — compute the slice bounds. Write the 8*i+7 : 8*i arithmetic and say where Verilog would do it for you.

Exercise 9 — write the generate-for. Express the same bank as a generate for and compile both.

Exercise 10 — prove equivalence. Instantiate both in one testbench and compare over the whole input space. Report the disagreement count.

Exercise 11 — the line counts. Compare the source lines of your two versions.

Exercise 12 — change N. Take both versions from 8 to 64 and describe the work each needed.

Exercise 13 — what the HDL cannot say. For each thing your flow generates, name where its input data lives.

Exercise 14 — a generator to delete. Find a generator in your project that duplicates a generate loop, and price its removal.

Exercise 15 — four costs. List the four costs a redundant generator adds, from the callout, against your own flow.

Exercise 16 — the returncodes. Compile four deliberately broken files and tabulate the returncodes.

Exercise 17 — count, not kind. Confirm on your compiler that the returncode tracks the error count.

Exercise 18 — fix a wrapper. Find a script in your flow that tests a specific non-zero returncode, and change it.

Exercise 19 — the kind from the message. Write the classifier that distinguishes a bind error from a syntax error, using the text.

Exercise 20 — two size inputs. Construct the hit[3:0] mismatch and say how it arises in a real generator.

Exercise 21 — one variable, used twice. Refactor your generator so the port width and the loop bound cannot disagree.

Exercise 22 — duplicate tags. Add a duplicate to your spec and say what the hardware does.

Exercise 23 — a property, not a string. Write one check over the simulation rather than over the generated text, and say what it proves that a string comparison cannot.

Exercise 24 — the header. Confirm your generated file carries a do-not-edit header, and note what Chapter 31.7 adds to it.

11. Summary

  • This is the best possible generator target by Chapter 31.1's metric — one shape, and a review discount of 0.016 at sixty-four lines.
  • The annotation's own line does not compile. tags[i] names an RTL array nothing declared, and all eight lines fail to bind.
  • That is an ambiguity, not a typo: the tags are either data in a spec or a signal in the design, and the two readings need different output.
  • If the values are data, emit the values — and sweep the whole input space, because a reversed spec list still compiles and still gives one hit per tag.
  • If the values are a signal, declare the array and compute the byte slice — which is arithmetic Verilog can do itself.
  • A generate for is one line to Python's eight, with the same shape count, and the two are the same circuit: zero disagreements over all 256 inputs.
  • The HDL version needs no second language, no do-not-edit discipline and no regeneration step, and scaling it is editing a parameter.
  • Generate only what the HDL cannot express, which is data living outside the HDL — a CSV, a register list, somebody else's port dump.
  • A generator that duplicates a language feature is pure cost: a second language, an uneditable file, a forgotten regeneration, and a review surface nobody is qualified to read.
  • Icarus's returncode varies with the number of problems, not their kind — 16, 4, 3, 0 — so there is no code meaning "syntax error", a wrapper must test rc != 0, and the classification has to come from the message.
  • hit[3:0] with eight assignments is two size inputs that disagreed; one variable used twice removes the whole class.
  • A duplicated tag produces two simultaneous hits, which is a hardware behaviour downstream logic does not expect. Validate before emitting.
  • Express the generator's contract as a property of the circuit — eight tags, eight matches, never a double hit — rather than as a comparison of strings.
  • N=1 emits [0:0] and is correct; N=0 emits output [-1:0] with no assignments and compiles cleanly.
  • An empty spec produces a module that instantiates, connects and drives nothing, reading back as z — a dangling net in synthesis, with no symptom pointing at the CSV.
  • Guard both ends of the size range. A maximum is a statement that somebody has thought about the size, and raising it is a one-line change with a reviewer attached.
  • Generators are tested at the size that motivated them. Test 0, 1, 2, N and N_max + 1.

The next chapter generates the artefact nobody wants to write by hand, where the failure is a single character in the wrong place.

Where this fits

Part of the Python curriculum.