Python Part II · Module 25 · Simulation Automation
Discovering Sources and Generating the Filelist
Step two of the runner is deciding what to compile. Hand-maintained filelists go stale the first time somebody adds a module and forgets, so the obvious move is to find the sources by pattern — and the obvious pattern is wrong in three separate ways at once. It returns the files in whatever order the filesystem feels like, it silently excludes every header, and it includes the testbench. None of the three produces an error message; two of them produce a compile failure that points somewhere else entirely.
Intermediate18 min readPythonPart IIAutomationSimulationVerification Flow
Part II · Module 25 · Chapter 25.2 · Discovering Sources and Generating the Filelist
1. Why Not a Hand-Written Filelist
HAND_WRITTEN = ["rtl/core/alu.sv", "rtl/core/fifo.sv", "rtl/pkg/axi_pkg.sv"]
ON_DISK = ["rtl/core/alu.sv", "rtl/core/fifo.sv", "rtl/core/regfile.sv",
"rtl/pkg/axi_pkg.sv", "rtl/if/axi_if.sv"]
missing = [f for f in ON_DISK if f not in HAND_WRITTEN]
stale = [f for f in HAND_WRITTEN if f not in ON_DISK]
print("in the filelist :", len(HAND_WRITTEN))
print("on disk :", len(ON_DISK))
print("never compiled :", missing)
print("listed, absent :", stale)
print()
print("the symptom of the first :", "an undefined module, named at elaboration")
print("the symptom of the second:", "a file-not-found, naming the filelist line")in the filelist : 3
on disk : 5
never compiled : ['rtl/core/regfile.sv', 'rtl/if/axi_if.sv']
listed, absent : []
the symptom of the first : an undefined module, named at elaboration
the symptom of the second: a file-not-found, naming the filelist lineTwo new modules exist and are never compiled. The second symptom is the kind one — the tool names the missing line. The first is the one that costs an afternoon, because "undefined module regfile" sends people to look at regfile.sv, which is correct, present and fine.
Generating the filelist from the tree removes the whole class. The next three sections are what the obvious generator gets wrong.
2. sorted Is Doing Work
srcs = sorted(Path("rtl").rglob("*.sv"))
write_filelist(run_dir / "files.f", srcs, incdirs)from pathlib import Path
LAYOUT = ["core/alu.sv", "core/fifo.sv", "pkg/axi_pkg.sv", "if/axi_if.sv",
"core/regfile.sv", "top/tb_top.sv", "core/alu_tb.sv", "inc/defines.svh"]
for rel in LAYOUT:
p = Path("rtl") / rel
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(f"// {rel}\n", encoding="utf-8")import make_tree
from pathlib import Path
found = list(Path("rtl").rglob("*.sv"))
ordered = sorted(found)
print("same files either way :", set(found) == set(ordered))
print("same order :", found == ordered or "not guaranteed")
print()
print("sorted() is a promise:")
print(" idempotent :", sorted(ordered) == ordered)
print(" first :", ordered[0].name)
print(" last :", ordered[-1].name)
print(" count :", len(ordered))same files either way : True
same order : not guaranteed
sorted() is a promise:
idempotent : True
first : alu.sv
last : tb_top.sv
count : 7rglob returns the same files; it does not promise an order. What it hands back is the filesystem's directory-iteration order, which depends on the filesystem, the order files were created, and sometimes on a hash of their names. On the machine that verified this chapter, tb_top.sv came back first from rglob and last from sorted — but that is a fact about this filesystem, not about Python, which is exactly why no raw listing is published here.
The consequence is what matters, and it is reproducible anywhere:
UNSORTED = ["rtl/top/tb_top.sv", "rtl/core/fifo.sv", "rtl/core/alu.sv",
"rtl/if/axi_if.sv", "rtl/pkg/axi_pkg.sv"]
print("a filelist in some filesystem's order:")
for line in UNSORTED:
print(" " + line)
print()
print("tb_top is compiled at position :", UNSORTED.index("rtl/top/tb_top.sv") + 1, "of", len(UNSORTED))
print("the package it imports, at :", UNSORTED.index("rtl/pkg/axi_pkg.sv") + 1)
print()
print("sorted, the same five files:")
for line in sorted(UNSORTED):
print(" " + line)
print()
print("two machines, same commit, same list:", sorted(UNSORTED) == sorted(reversed(UNSORTED)))a filelist in some filesystem's order:
rtl/top/tb_top.sv
rtl/core/fifo.sv
rtl/core/alu.sv
rtl/if/axi_if.sv
rtl/pkg/axi_pkg.sv
tb_top is compiled at position : 1 of 5
the package it imports, at : 5
sorted, the same five files:
rtl/core/alu.sv
rtl/core/fifo.sv
rtl/if/axi_if.sv
rtl/pkg/axi_pkg.sv
rtl/top/tb_top.sv
two machines, same commit, same list: TrueTwo things are wrong with the unsorted version and only one of them is obvious.
The obvious one: the testbench top is compiled first and the package it imports is compiled last. Some tools tolerate that and some do not, which is worse than if none did.
The one that costs more: the filelist is now different on different machines. Two engineers, same commit, same command, and the compile order differs — so a compile error that reproduces for one of them does not reproduce for the other. Chapter 24.6 built an entire record format around answering what changed?, and an unsorted filelist means the answer is "the filelist" on every single comparison.
sorted() costs nothing and converts the source set into a function of the commit, which is the property the whole module needs.
3. The Pattern Misses Every Header
from pathlib import Path
LAYOUT = ["core/alu.sv", "core/fifo.sv", "pkg/axi_pkg.sv", "if/axi_if.sv",
"core/regfile.sv", "top/tb_top.sv", "core/alu_tb.sv", "inc/defines.svh"]
for rel in LAYOUT:
p = Path("rtl") / rel
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(f"// {rel}\n", encoding="utf-8")import make_tree
from pathlib import Path
root = Path("rtl")
for pattern in ("*.sv", "*.svh", "*.sv*"):
matches = sorted(root.rglob(pattern))
print(f"{pattern:<8} {len(matches):>2} file(s) {[p.name for p in matches][:3]}")
print()
headers = sorted(root.rglob("*.svh"))
print("headers found by *.sv :", [p for p in sorted(root.rglob('*.sv')) if p.suffix == '.svh'])
print("headers that exist :", [p.name for p in headers])*.sv 7 file(s) ['alu.sv', 'alu_tb.sv', 'fifo.sv']
*.svh 1 file(s) ['defines.svh']
*.sv* 8 file(s) ['alu.sv', 'alu_tb.sv', 'fifo.sv']
headers found by *.sv : []
headers that exist : ['defines.svh']*.sv does not match defines.svh. Glob patterns match the whole name, and .svh is not .sv.
That is correct behaviour and it is also usually a bug, because the header is real and the compile needs it. The failure arrives as an undefined macro or an unknown type, reported at the first line that uses it — a file that is in the filelist, compiles fine on its own, and has nothing wrong with it.
The fix is not *.sv*, which would put the headers in the filelist as compilation units. A SystemVerilog header is meant to be \include`d, not compiled, and listing it separately can produce duplicate definitions. Headers belong on the include path:
from pathlib import Path
LAYOUT = ["core/alu.sv", "core/fifo.sv", "pkg/axi_pkg.sv", "if/axi_if.sv",
"core/regfile.sv", "top/tb_top.sv", "core/alu_tb.sv", "inc/defines.svh"]
for rel in LAYOUT:
p = Path("rtl") / rel
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(f"// {rel}\n", encoding="utf-8")import make_tree
from pathlib import Path
root = Path("rtl")
sources = sorted(root.rglob("*.sv"))
headers = sorted(root.rglob("*.svh"))
incdirs = sorted({h.parent for h in headers})
print("compile these :", len(sources), "files")
print("include from :", [str(d) for d in incdirs])
print()
print("no header is in the source list:", not any(p.suffix == ".svh" for p in sources))
print("every header's directory is on the include path:",
all(h.parent in incdirs for h in headers))compile these : 7 files
include from : ['rtl/inc']
no header is in the source list: True
every header's directory is on the include path: TrueDeriving incdirs from where the headers actually are beats maintaining a second list, for the same reason generating the filelist beats maintaining the first one. The set collapses several headers in one directory to a single +incdir+, and sorted puts it back in a deterministic order.
4. And It Includes the Testbench
from pathlib import Path
LAYOUT = ["core/alu.sv", "core/fifo.sv", "pkg/axi_pkg.sv", "if/axi_if.sv",
"core/regfile.sv", "top/tb_top.sv", "core/alu_tb.sv", "inc/defines.svh"]
for rel in LAYOUT:
p = Path("rtl") / rel
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(f"// {rel}\n", encoding="utf-8")import make_tree
from pathlib import Path
EXCLUDE_DIRS = {"top", "tb"}
EXCLUDE_SUFFIXES = ("_tb.sv", "_test.sv")
found = sorted(Path("rtl").rglob("*.sv"))
def is_rtl(path):
if any(part in EXCLUDE_DIRS for part in path.parts):
return False
return not path.name.endswith(EXCLUDE_SUFFIXES)
rtl = [p for p in found if is_rtl(p)]
dropped = [p for p in found if not is_rtl(p)]
print("everything *.sv matched:", [p.name for p in found])
print()
print("kept as RTL :", [p.name for p in rtl])
print("excluded :", [p.name for p in dropped])
print("why excluded :",
{p.name: ("directory" if any(x in EXCLUDE_DIRS for x in p.parts) else "suffix")
for p in dropped})everything *.sv matched: ['alu.sv', 'alu_tb.sv', 'fifo.sv', 'regfile.sv', 'axi_if.sv', 'axi_pkg.sv', 'tb_top.sv']
kept as RTL : ['alu.sv', 'fifo.sv', 'regfile.sv', 'axi_if.sv', 'axi_pkg.sv']
excluded : ['alu_tb.sv', 'tb_top.sv']
why excluded : {'alu_tb.sv': 'suffix', 'tb_top.sv': 'directory'}Two exclusion mechanisms, because teams use both conventions and usually both at once: a tb/ directory and a _tb.sv suffix for the odd bench that lives beside its module.
Reporting why each file was excluded is the part worth copying, and it is the same argument Chapter 24.4 Section 3 made about a sweep: the first time this drops a file it should not, the reason is the line of policy to change, and without it somebody reads the whole function.
5. Deterministic and Wrong
from pathlib import Path
LAYOUT = ["core/alu.sv", "core/fifo.sv", "pkg/axi_pkg.sv", "if/axi_if.sv",
"core/regfile.sv", "top/tb_top.sv", "core/alu_tb.sv", "inc/defines.svh"]
for rel in LAYOUT:
p = Path("rtl") / rel
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(f"// {rel}\n", encoding="utf-8")import make_tree
from pathlib import Path
found = sorted(p for p in Path("rtl").rglob("*.sv")
if not p.name.endswith("_tb.sv") and "top" not in p.parts)
print("alphabetical:")
for p in found:
print(" " + str(p))
pkg_at = next(i for i, p in enumerate(found) if "pkg" in p.parts)
print()
print("the package is at position:", pkg_at + 1, "of", len(found))
print("files compiled before it :", pkg_at)
print("…every one of which may import it")alphabetical:
rtl/core/alu.sv
rtl/core/fifo.sv
rtl/core/regfile.sv
rtl/if/axi_if.sv
rtl/pkg/axi_pkg.sv
the package is at position: 5 of 5
files compiled before it : 4
…every one of which may import itSorting made the order stable. It did not make the order correct.
A SystemVerilog package must be analysed before anything that imports it, and an interface before anything that instantiates it. Alphabetical order knows nothing about that, and axi_pkg.sv sorting last is not bad luck — packages routinely sort after the modules that use them.
Determinism and correctness are separate properties, and sorted() only buys the first. The usual fix is a declared tier order:
from pathlib import Path
LAYOUT = ["core/alu.sv", "core/fifo.sv", "pkg/axi_pkg.sv", "if/axi_if.sv",
"core/regfile.sv", "top/tb_top.sv", "core/alu_tb.sv", "inc/defines.svh"]
for rel in LAYOUT:
p = Path("rtl") / rel
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(f"// {rel}\n", encoding="utf-8")import make_tree
from pathlib import Path
TIERS = ("pkg", "if", "core")
def tier_of(path):
for index, name in enumerate(TIERS):
if name in path.parts:
return index
return len(TIERS)
found = sorted(p for p in Path("rtl").rglob("*.sv")
if not p.name.endswith("_tb.sv") and "top" not in p.parts)
ordered = sorted(found, key=lambda p: (tier_of(p), str(p)))
print("compile order:")
for p in ordered:
print(f" tier {tier_of(p)} {p}")
print()
print("still deterministic:", ordered == sorted(found, key=lambda p: (tier_of(p), str(p))))
print("packages first :", tier_of(ordered[0]) == 0)
print("untiered go last :", tier_of(ordered[-1]) <= len(TIERS))compile order:
tier 0 rtl/pkg/axi_pkg.sv
tier 1 rtl/if/axi_if.sv
tier 2 rtl/core/alu.sv
tier 2 rtl/core/fifo.sv
tier 2 rtl/core/regfile.sv
still deterministic: True
packages first : True
untiered go last : TrueThe key is a tuple — (tier, path) — so files sort by tier first and alphabetically within a tier. Chapter 5.5's sort key, doing real work: one expression gives both the dependency order and the tie-break that keeps it reproducible.
A file in no tier gets len(TIERS) and lands at the end, which is the safe default: a new directory nobody has classified compiles last rather than breaking the order silently.
6. Nothing Matched
from pathlib import Path
Path("empty_rtl").mkdir(exist_ok=True)
found = sorted(Path("empty_rtl").rglob("*.sv"))
print("rglob returned :", found)
print("it raised :", "nothing")
print("len :", len(found))
print()
print("what happens next, unguarded:")
print(" an empty files.f is written")
print(" the compiler is invoked and succeeds, having compiled nothing")
print(" elaboration fails with 'cannot find top module tb_top'")
print(" somebody looks at tb_top.sv")rglob returned : []
it raised : nothing
len : 0
what happens next, unguarded:
an empty files.f is written
the compiler is invoked and succeeds, having compiled nothing
elaboration fails with 'cannot find top module tb_top'
somebody looks at tb_top.svA pattern that matches nothing is not an error in Python, and it must become one here. Under Chapter 25.1's taxonomy this is ERROR, not COMPILE_ERROR — nothing about the design was tested, and the fault is a wrong root path or a wrong pattern:
from pathlib import Path
LAYOUT = ["core/alu.sv", "core/fifo.sv", "pkg/axi_pkg.sv", "if/axi_if.sv",
"core/regfile.sv", "top/tb_top.sv", "core/alu_tb.sv", "inc/defines.svh"]
for rel in LAYOUT:
p = Path("rtl") / rel
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(f"// {rel}\n", encoding="utf-8")import make_tree
from pathlib import Path
class FilelistError(Exception):
"""No sources, or a source set that cannot be compiled."""
def discover(root, pattern="*.sv", min_files=1):
root = Path(root)
if not root.is_dir():
raise FilelistError(f"source root {root} does not exist or is not a directory")
found = sorted(root.rglob(pattern))
if len(found) < min_files:
raise FilelistError(
f"{len(found)} file(s) matched {pattern!r} under {root} — "
f"expected at least {min_files}; check the root and the pattern")
return found
Path("empty_rtl").mkdir(exist_ok=True)
for root in ("rtl", "empty_rtl", "no_such_root"):
try:
found = discover(root)
print(f"{root:<14} {len(found)} file(s)")
except FilelistError as exc:
print(f"{root:<14} FilelistError: {exc}")rtl 7 file(s)
empty_rtl FilelistError: 0 file(s) matched '*.sv' under empty_rtl — expected at least 1; check the root and the pattern
no_such_root FilelistError: source root no_such_root does not exist or is not a directoryTwo distinct states, two messages, the same argument Chapter 24.5 Section 6 made about directories. And min_files is worth more than it looks: a project with four hundred source files that suddenly discovers three has not had three deleted — somebody has pointed the root at the wrong place, and min_files=50 catches that where min_files=1 does not.
7. Writing the File
from pathlib import Path
LAYOUT = ["core/alu.sv", "core/fifo.sv", "pkg/axi_pkg.sv", "if/axi_if.sv",
"core/regfile.sv", "top/tb_top.sv", "core/alu_tb.sv", "inc/defines.svh"]
for rel in LAYOUT:
p = Path("rtl") / rel
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(f"// {rel}\n", encoding="utf-8")import make_tree
from pathlib import Path
def write_filelist(path, sources, incdirs=(), defines=()):
"""Write a -f filelist. Inputs absolute, per Chapter 23.7 §3."""
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
lines = ["// generated — do not edit by hand"]
for name, value in defines:
lines.append(f"+define+{name}={value}" if value is not None else f"+define+{name}")
for d in incdirs:
lines.append(f"+incdir+{Path(d).resolve()}")
for s in sources:
lines.append(str(Path(s).resolve()))
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
return len(lines)
root = Path("rtl")
sources = sorted(p for p in root.rglob("*.sv")
if not p.name.endswith("_tb.sv") and "top" not in p.parts)
incdirs = sorted({h.parent for h in root.rglob("*.svh")})
run_dir = Path("runs/axi_smoke_1839274")
count = write_filelist(run_dir / "files.f", sources, incdirs, defines=[("DUMP_WAVES", "1")])
text = (run_dir / "files.f").read_text(encoding="utf-8")
for line in text.splitlines():
if "/rtl/" in line:
head, tail = line.split("/rtl/", 1)
prefix = "+incdir+" if head.startswith("+incdir+") else ""
print(f" {prefix}<abs>/{tail}")
else:
print(" " + line)
print()
print("lines written :", count)
print("every source is absolute:", all(l.startswith("/") for l in text.splitlines()
if l.endswith(".sv"))) // generated — do not edit by hand
+define+DUMP_WAVES=1
+incdir+<abs>/inc
<abs>/core/alu.sv
<abs>/core/fifo.sv
<abs>/core/regfile.sv
<abs>/if/axi_if.sv
<abs>/pkg/axi_pkg.sv
lines written : 8
every source is absolute: TrueThe display substitutes <abs> because the real prefix differs on every machine; the file on disk carries the full path, which is the point.
Every path is resolved to an absolute one, and that is Chapter 23.7 Section 3's rule arriving where it bites hardest. The filelist is read by the compiler, which Chapter 23.7 runs with cwd=run_dir — so a relative rtl/core/alu.sv inside it would be resolved against the run directory, where no rtl/ exists. The tool then reports "cannot find rtl/core/alu.sv", and the file is sitting exactly where it always was.
The first line says the file is generated. A files.f in a run directory with no provenance gets hand-edited by somebody debugging at 9pm, and their edit vanishes on the next run with no explanation.
Defines come before includes, which come before sources, because a +define+ has to be in effect before a file that tests it is read. Within each group the order is the one Section 5 established.
8. The Filelist as a Fingerprint
from pathlib import Path
LAYOUT = ["core/alu.sv", "core/fifo.sv", "pkg/axi_pkg.sv", "if/axi_if.sv",
"core/regfile.sv", "top/tb_top.sv", "core/alu_tb.sv", "inc/defines.svh"]
for rel in LAYOUT:
p = Path("rtl") / rel
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(f"// {rel}\n", encoding="utf-8")import hashlib
import make_tree
from pathlib import Path
def source_fingerprint(sources, root):
"""Identify the source SET, independently of where the tree is checked out."""
rel = sorted(str(Path(s).relative_to(root)) for s in sources)
return hashlib.sha256("\n".join(rel).encode("utf-8")).hexdigest()
root = Path("rtl")
sources = sorted(p for p in root.rglob("*.sv") if not p.name.endswith("_tb.sv"))
before = source_fingerprint(sources, root)
(root / "core" / "newmod.sv").write_text("// new\n", encoding="utf-8")
after = source_fingerprint(
sorted(p for p in root.rglob("*.sv") if not p.name.endswith("_tb.sv")), root)
print("before adding a file:", before[:12])
print("after :", after[:12])
print("changed :", before != after)
print()
print("relative, so a different checkout gives the same digest:",
source_fingerprint(sources, root) == before)before adding a file: 8541489da628
after : 91e9e7b89bbb
changed : True
relative, so a different checkout gives the same digest: TrueThe digests are stable because the input is: sorted, relative paths joined with newlines — Chapter 24.6 Section 6's canonicalisation rule applied to a different kind of record.
relative_to(root) is what makes it portable. Absolute paths would make the digest a property of the checkout directory, so two engineers with the same commit would get different fingerprints and the comparison would be worthless — the same mistake, in a different costume, as storing PATH as one string.
What this buys sits in the run record next to Chapter 24.6's environment fingerprint. "Did the source set change between these two runs?" becomes one comparison, and a regression whose runs do not all share a source fingerprint is not one regression.
It identifies the set, not the contents: a file edited in place keeps the same digest. That is the right granularity here, because Chapter 24.6 Section 8 already drew this line — the RTL revision is git's job, and this answers the narrower question of whether the filelist changed.
9. Predict, Run, Observe
from pathlib import Path
LAYOUT = ["core/alu.sv", "core/fifo.sv", "pkg/axi_pkg.sv", "if/axi_if.sv",
"core/regfile.sv", "top/tb_top.sv", "core/alu_tb.sv", "inc/defines.svh"]
for rel in LAYOUT:
p = Path("rtl") / rel
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(f"// {rel}\n", encoding="utf-8")import make_tree
from pathlib import Path
root = Path("rtl")
# A
print("A:", set(root.rglob("*.sv")) == set(sorted(root.rglob("*.sv"))))
# B
print("B:", sorted(root.rglob("*.sv")) == sorted(sorted(root.rglob("*.sv"))))
# C
print("C:", len(list(root.rglob("*.sv"))), len(list(root.rglob("*.svh"))))
# D
print("D:", any(p.suffix == ".svh" for p in root.rglob("*.sv")))
# E
print("E:", sorted(p.name for p in root.rglob("*.sv*"))[:2])
# F
print("F:", list(Path("rtl").rglob("*.vhdl")))
# G
TIERS = ("pkg", "if", "core")
key = lambda p: (next((i for i, t in enumerate(TIERS) if t in p.parts), len(TIERS)), str(p))
print("G:", [p.name for p in sorted(root.rglob("*.sv"), key=key)][:2])
# H
print("H:", Path("rtl/core/alu.sv").resolve().is_absolute())The verified output:
A: True
B: True
C: 7 1
D: False
E: ['alu.sv', 'alu_tb.sv']
F: []
G: ['axi_pkg.sv', 'axi_if.sv']
H: TrueA — the same files, whatever the order.
B — sorted is idempotent, which is what makes it a promise rather than a shuffle.
C — seven .sv and one .svh. Section 8's extra module is not here: every example in this chapter starts from a fresh tree.
D — no header is matched by *.sv. Section 3.
E — *.sv* does match them, which is why it is the wrong fix.
F — a pattern matching nothing returns an empty list and raises nothing.
G — the tier key puts the package and the interface first.
H — .resolve() is what makes a filelist entry survive cwd=.
10. Exercises
Exercise 1 — the stale list. Compare a hand-written filelist against the tree. Report both directions.
Exercise 2 — two symptoms. Say which of the two failures is worse to debug and why.
Exercise 3 — order is not promised. Show set(rglob) == set(sorted) and say what that does and does not establish.
Exercise 4 — the consequence. Take an unsorted list and report where the testbench top and the package land.
Exercise 5 — two machines. Explain why an unsorted filelist makes Chapter 24.6's "what changed?" useless.
Exercise 6 — a function of the commit. State in one sentence what sorted() converts the source set into.
Exercise 7 — the missing headers. Count *.sv against *.svh in your own tree and say what a naive pattern drops.
Exercise 8 — not *.sv*. Explain why widening the pattern is the wrong fix.
Exercise 9 — derive the include path. Build incdirs from where the headers are, and confirm no header is in the source list.
Exercise 10 — exclude the bench. Implement both a directory rule and a suffix rule. Report why each file was excluded.
Exercise 11 — the claim about names. Find a testbench filename your rule misses. Say how you found it.
Exercise 12 — deterministic and wrong. Show a package sorting after its importers, and say why alphabetical cannot know.
Exercise 13 — tiers. Implement the tuple sort key. Confirm it is still deterministic.
Exercise 14 — the untiered default. Say what should happen to a file in no tier, and why.
Exercise 15 — the approximation. Name a tree shape where tiers fail, and say what the real fix is.
Exercise 16 — nothing matched. Trace what an unguarded empty filelist does, step by step, to the error somebody finally sees.
Exercise 17 — two states. Implement the missing-root and no-matches checks with separate messages.
Exercise 18 — min_files. Choose a threshold for your project and say what it catches that 1 does not.
Exercise 19 — absolute. Write a filelist with relative paths, run the compiler with cwd=run_dir, and record the error.
Exercise 20 — the generated header. Say what happens without the do-not-edit line.
Exercise 21 — fingerprint. Compute a source fingerprint and show it changing when a file is added.
Exercise 22 — relative_to. Explain why absolute paths would make the fingerprint worthless, and name the Chapter 24.6 mistake it mirrors.
11. Summary
- A hand-written filelist goes stale, and the expensive symptom is an "undefined module" that sends people to a file which is perfectly fine.
rglobreturns the same files in no promised order — the filesystem's, which varies by filesystem, creation order and sometimes a name hash.sorted()is the load-bearing word in the annotation. It is idempotent, so the source set becomes a function of the commit.- An unsorted filelist differs between machines, so a compile error reproduces for one engineer and not the other, and every "what changed?" comparison says "the filelist".
*.svdoes not match.svh, so the obvious pattern silently drops every header and the compile fails at the first use of a macro.*.sv*is the wrong fix — headers belong on the include path, not in the source list.- Derive
incdirsfrom where the headers are, with asetto collapse duplicates andsortedto keep it deterministic. - A naive pattern also sweeps in the testbench; exclude by directory and by suffix, because teams use both.
- Report why each file was excluded, so the first wrong exclusion names the line of policy to change.
- An exclusion rule is a claim about filenames — run it over the real tree and read both lists.
- Deterministic and wrong are compatible. A package must be analysed before its importers, and it routinely sorts after them.
- A
(tier, path)tuple sort key gives dependency order and a reproducible tie-break in one expression; untiered files go last. - Tiers are an approximation, and the real answer is the tool's own single-pass mode.
- A pattern matching nothing returns
[]and raises nothing, which must becomeERROR— the design was never tested. - Distinguish a missing root from a no-match, and set
min_fileshigh enough to catch a root pointed at the wrong place. - Resolve every path in the filelist to an absolute one, because the compiler runs with
cwd=run_dir. - Mark the file as generated, or somebody hand-edits it and their change vanishes.
- Order the file as defines, then includes, then sources.
- Fingerprint the source set from sorted relative paths; absolute ones would make the digest a property of the checkout.
- The fingerprint identifies the set, not the contents — the revision is git's job.
The next chapter takes this filelist and compiles it, keeping the compile log and treating a compile failure as its own result class.
Where this fits
Part of the Python curriculum.
