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:
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}") 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.125One 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
for i in range(n):
print(f" assign hit[{i}] = (tag == tags[{i}]);", file=f)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]))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: 17tags 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:
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)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: 03. If the Values Are Data, Emit the Values
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)compile returncode: 0
tags that matched something: 8
tags that matched more than one: 0
tag=32 hit=00000100Compiled, 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:
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")// 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*iThis 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
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)compile returncode: 0
tag values compared: 256
disagreements : 0Zero disagreements over the entire input space. The two modules are the same circuit, established by exhaustion rather than by claiming it.
Now price them:
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")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 parameterIdentical 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:
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")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 HDLGenerate 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:
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")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 != 0Sixteen, 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
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")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: TrueThe 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:
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}")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.
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)compile returncode: 0
hit=z width=1
the module compiled and connectedAn 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:
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"])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
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:
A: 1
B: 1
C: tags[23:16] vs tags[8*i +: 8]
D: True FalseA — 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 foris 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=1emits[0:0]and is correct;N=0emitsoutput [-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.
