Linux & Bash · Part II · Module 23 · Navigating RTL and Verification Repositories
Finding the Real Source of a Module
Searching for a module by name is easy and usually gives more than one answer. This chapter is about the part nobody teaches: deciding which of the answers is the file the compiler actually read — measured on a repository with three copies, and confirmed a second way by the tool itself.
Intermediate21 min readLinuxPart IIgrepfindFilelists
Part II · Module 23 · Chapter 23.3 · Finding the Real Source of a Module
A log names a module. You want to read it. This takes four seconds when there is one copy and costs an afternoon when there are three — and three is the normal number.
1. The Two Obvious Searches, and Why Both Are Insufficient
By file name, which is the registry's first suggestion and assumes a convention:
$ find . -name 'axi_master*.sv'
./ip/vendor_axi/rtl/axi_master.sv
./rtl/axi/axi_master.sv
./run_2026_10_01/axi_master.svBy definition, which does not assume the convention and is therefore better:
$ /usr/bin/grep -rn 'module axi_master' --include='*.sv' .
./run_2026_10_01/axi_master.sv:2:module axi_master (input logic clk, input logic rst_n, output logic [7:0] stat);
./rtl/axi/axi_master.sv:2:module axi_master (input logic clk, input logic rst_n, output logic [7:0] stat);
./ip/vendor_axi/rtl/axi_master.sv:2:module axi_master (input logic clk, input logic rst_n, output logic [7:0] stat);The full path /usr/bin/grep is deliberate, and it is not pedantry. On the machine these transcripts were taken from, a plain grep is a shell function dispatching to ugrep, which returns two hits for that same command because it silently skips directories listed in .gitignore — and run_2026_10_01/ is one. Chapter 23.5 §3 measures that divergence; for now the thing to know is that "how many copies are there" has a different answer depending on which grep you have, which is why this chapter pins it.
Three answers each. Searching by definition is the one to prefer, for a reason worth stating: a module does not have to live in a file named after it. Nothing in the language requires it, packages and interfaces frequently share a file, and generated code routinely puts twenty modules in one. So find -name answers a question about file names while grep 'module <x>' answers a question about the design.
But both return three, and the three files differ:
rtl/axi/axi_master.svdrivesstatto8'hA1ip/vendor_axi/rtl/axi_master.svis the vendor's originalrun_2026_10_01/axi_master.svis an empty stub somebody left behind
Read the wrong one and every conclusion is about code that is not running.
From a module name to the file that was actually compiled
2. The Authority Is the Filelist
The compiler did not search the tree. It was handed a list:
$ cat filelists/chip.f
rtl/core/chip_top.sv
rtl/axi/axi_master.sv
rtl/uart/uart_tx.sv
tb/tests/tb_chip.svSo the question "which axi_master.sv is real" is really "which of the three candidates appears in the list the tool was given", and that is a set intersection:
$ grep -rl --include='*.sv' -E '^[[:space:]]*module[[:space:]]+axi_master\b' . |
> sed 's|^\./||' | sort > /tmp/defs
$ sort filelists/chip.f > /tmp/fl
$ comm -12 /tmp/defs /tmp/fl
rtl/axi/axi_master.sv
$ comm -23 /tmp/defs /tmp/fl
ip/vendor_axi/rtl/axi_master.sv
run_2026_10_01/axi_master.svOne line of output from comm -12, and it is the answer.
comm compares two sorted files and prints three columns: lines only in the first, lines only in the second, lines in both. -12 suppresses columns 1 and 2, leaving only the common lines; -23 suppresses 2 and 3, leaving only those unique to the first file — which is the far more interesting list, because it is "definitions that are not compiled". Chapter 13.8 introduced comm; the sed 's|^\./||' is there because grep -r . prefixes ./ and the filelist does not, and two paths that differ by a prefix do not intersect.
3. Confirming It a Second Way, From the Tool
The intersection is a claim about files. The tool knows the truth, and it is worth asking it, because two independent methods agreeing is much stronger than one method you trust.
The simplest version costs one line in the design, and many real projects have it already:
initial $display("AXI: elaborated %m from rtl/axi/axi_master.sv");$ vvp work/sim.vvp < /dev/null
AXI: elaborated tb_chip.dut.u_axi from rtl/axi/axi_master.sv
UART: elaborated tb_chip.dut.u_uart
TB: scope is tb_chip
TB: status=fd
TEST PASSED%m expands to the hierarchical scope name, so the message says both which instance and — because the string was written into the source — which file. The two methods agree on rtl/axi/axi_master.sv.
Three more ways to ask the tool, in order of how generally they work:
`__FILE__ and `__LINE__, which SystemVerilog defines and which beat a hand-written path because they cannot go stale when a file is moved:
initial $display("%m elaborated from %s:%0d", `__FILE__, `__LINE__);The compiler's own file list. Most tools can report what they read — a -v/-verbose flag, a .log listing compiled units, or a dependency file. Icarus does not print one, so for this machine the honest answer is the %m approach above; on a commercial simulator this is the first thing to reach for, and the flag names differ enough that the tool's own documentation is the only reliable source.
And the generated dependency output, when a flow produces one — a .d file, a work/ manifest, a simv.daidir listing. These are the ground truth because the tool wrote them, and they are in a generated directory, which is the one legitimate reason to look inside one.
4. When the Filelist Is Not the Whole Story
Three mechanisms put a file into a compile without naming it, and each one breaks §2's intersection. They are worth recognising by their syntax.
-y <dir> with +libext+.sv — a library search path. The tool resolves any unresolved module name by looking for <name>.sv in that directory. So a filelist of four lines can compile forty files, and the only way to know which is to let the tool tell you.
grep -nE '^\s*-y|\+libext' filelists/*.f # does this flow use library search?+incdir+<dir> with `include — textual inclusion. The included file is not a compilation unit of its own, so it never appears in any list of compiled files, and grep 'module x' finds it while the filelist never mentions it.
grep -rn '`include' --include='*.sv' rtl tb | sed 's/.*`include *//' | sort -uAnd a filelist that includes another filelist, usually -f or -F recursively:
grep -nE '^\s*-[fF] ' filelists/*.fThe third is the commonest and the easiest to miss, because the top-level filelist looks short and complete.
Before writing a resolver, there is a question that has to be settled first, and getting it wrong is the standard way these tools fail.
So the resolver needs the base directory to be a decision rather than an assumption:
#!/usr/bin/env bash
# flatten-filelist — print every source file a filelist names, following -f/-F
#
# usage: flatten-filelist [-b cwd|filelist] <filelist>
# -b cwd resolve relative entries against the current directory
# (the default, and what iverilog/vcs/xrun actually do)
# -b filelist resolve against each filelist's own directory
# (some home-grown flows; check yours before trusting it)
# exit: 0 ok · 2 a filelist is missing or usage is wrong
set -o nounset
base=cwd
while getopts ':b:' opt; do
case $opt in
b) case $OPTARG in cwd|filelist) base=$OPTARG ;;
*) printf 'flatten-filelist: -b must be cwd or filelist\n' >&2; exit 2 ;;
esac ;;
:) printf 'flatten-filelist: -b needs a value\n' >&2; exit 2 ;;
\?) printf 'flatten-filelist: unknown option -%s\n' "$OPTARG" >&2; exit 2 ;;
esac
done
shift $(( OPTIND - 1 ))
root=$(pwd -P)
flatten() {
local fl=$1 here line path
here=$(cd -- "$(dirname -- "$fl")" && pwd -P) || return 2
local anchor
case $base in cwd) anchor=$root ;; filelist) anchor=$here ;; esac
while IFS= read -r line || [ -n "$line" ]; do
line=${line%%#*}
line=${line%$'\r'}
case $line in
''|*[^[:space:]]*) : ;;
*) continue ;;
esac
[ -n "${line//[[:space:]]/}" ] || continue
case $line in
-[fF]*)
path=${line#-[fF]}; path=${path# }
case $path in /*) : ;; *) path=$anchor/$path ;; esac
[ -r "$path" ] || { printf 'flatten-filelist: missing filelist: %s\n' "$path" >&2; return 2; }
flatten "$path" || return 2 ;;
-*|+*) continue ;;
*)
path=$line
case $path in /*) : ;; *) path=$anchor/$path ;; esac
printf '%s\n' "$path"
[ -e "$path" ] || printf 'flatten-filelist: NOT FOUND: %s\n' "$path" >&2 ;;
esac
done < "$fl"
}
fl=${1:?usage: flatten-filelist [-b cwd|filelist] <filelist>}
[ -r "$fl" ] || { printf 'flatten-filelist: cannot read %s\n' "$fl" >&2; exit 2; }
flatten "$fl"Run from the repository root, with a nested filelist, a +incdir+, a -y, a comment, and no trailing newline on the inner file:
$ cat filelists/top.f
rtl/core/chip_top.sv
+incdir+rtl/include
-y ip/vendor_axi/rtl
# a comment
-f filelists/sub.f
$ ./flatten-filelist filelists/top.f
/home/engineer/chip_a/rtl/core/chip_top.sv
/home/engineer/chip_a/rtl/axi/axi_master.sv
/home/engineer/chip_a/rtl/uart/uart_tx.svThree entries, the options skipped, the comment skipped, and uart_tx.sv present even though sub.f has no final newline. A missing nested filelist exits 2 and names it:
$ ./flatten-filelist filelists/bad.f > /dev/null
flatten-filelist: missing filelist: /home/engineer/chip_a/nope.f
$ echo $?
2Seven decisions worth naming:
The base directory is an option with a documented default, rather than a guess baked into the code. Chapter 21.3 is the general argument; this is the case where the wrong default produces plausible nonsense rather than an error.
getopts with :b:, so -b without a value is a usage error rather than silently consuming the filename — Chapter 21.2 §5 measured exactly that failure.
root=$(pwd -P) is captured once, before any recursion. Capturing it inside flatten would be fine here, but reading it once makes it obvious that every relative entry resolves against one directory rather than drifting as the recursion descends.
line=${line%$'\r'} strips a trailing carriage return with parameter expansion instead of forking tr per line — Chapter 8.3. A filelist that has been through Windows is common, and a path ending in \r fails to open with an error that does not mention the \r.
|| [ -n "$line" ] on the read, so a final line with no newline is still processed. Chapter 17.5 §4 measured that it is otherwise silently dropped — and a filelist is exactly the kind of file people hand-edit.
-*|+*) continue so +incdir+, -y, +define+ and -sv are skipped rather than treated as paths. This is a deliberate under-approximation, and the -y case genuinely cannot be resolved without the tool.
And every emitted path is checked with [ -e ], warning on stderr while still printing it. That is what turns the wrong-base bug from invisible into a line of output — the check I did not have when I wrote it the first time.
5. The Script
Combining §1, §2 and §4 into the thing to actually run:
#!/usr/bin/env bash
# which-module — where is module <name> defined, and which copy is compiled?
# usage: which-module <module> [filelist]
# exit: 0 exactly one compiled definition · 1 none or several · 2 bad usage
set -o nounset
mod=${1:?usage: which-module <module> [filelist]}
fl=${2:-}
printf -- '-- definitions of %s\n' "$mod"
defs=$(grep -rl --include='*.sv' --include='*.v' \
-E "^[[:space:]]*module[[:space:]]+${mod}[^a-zA-Z0-9_]" . 2>/dev/null |
sed 's|^\./||' | sort)
[ -n "$defs" ] || { printf ' none found\n' >&2; exit 1; }
printf ' %s\n' $defs
[ -n "$fl" ] || { printf '\n(no filelist given — cannot say which is compiled)\n'; exit 1; }
printf -- '\n-- cross-referenced against %s\n' "$fl"
if grep -q '\$' "$fl"; then
printf ' WARNING: %s contains a variable; the comparison may be wrong\n' "$fl" >&2
fi
compiled=$(comm -12 <(printf '%s\n' $defs) <(sed 's|^\./||' "$fl" | sort))
case $(printf '%s' "$compiled" | grep -c .) in
0) printf ' COMPILED: none of them — check -y, `include, or a nested -f\n'; exit 1 ;;
1) printf ' COMPILED: %s\n' "$compiled" ;;
*) printf ' COMPILED: SEVERAL — the design will not elaborate:\n'
printf ' %s\n' $compiled; exit 1 ;;
esac
printf -- '\n-- present but NOT compiled\n'
comm -23 <(printf '%s\n' $defs) <(sed 's|^\./||' "$fl" | sort) |
sed 's/^/ /' || true$ ./which-module axi_master filelists/chip.f
-- definitions of axi_master
ip/vendor_axi/rtl/axi_master.sv
rtl/axi/axi_master.sv
run_2026_10_01/axi_master.sv
-- cross-referenced against filelists/chip.f
COMPILED: rtl/axi/axi_master.sv
-- present but NOT compiled
ip/vendor_axi/rtl/axi_master.sv
run_2026_10_01/axi_master.svFour details that are the difference between a demo and a tool:
The "several" case exits 1 with its own message. Two compiled definitions of one module is not an ambiguity for you to resolve by preference — it is a design that will not elaborate, and saying so is more useful than picking one. Chapter 21.4's principle that a status should distinguish outcomes a caller would handle differently.
grep -q '\$' warns rather than failing. The comparison may still be right; the user needs to know it might not be. A tool that refuses to answer when it could answer imperfectly gets replaced by a worse one.
$(printf '%s' "$compiled" | grep -c .) counts non-empty lines, not wc -l, because an empty $compiled still has one line as far as wc -l is concerned once it goes through printf. That off-by-one is the sort of thing that makes a zero-result case report "1" — and it is only visible by running it on a module that does not exist.
And printf ' %s\n' $defs is deliberately unquoted, which is the one place in this course where that is correct: it relies on word splitting to print one path per line. It is also the construct that breaks on a path containing a space, so the honest version keeps the paths in an array:
defs=()
while IFS= read -r d; do defs+=("$d"); done < <(grep -rl … | sed … | sort)
printf ' %s\n' "${defs[@]}"Chapter 19.5 is why, and Chapter 19.4 is the < <( ) that makes the array survive the loop.
6. The Same Problem for a Testbench File
Identical method, different keyword. A UVM test or sequence is a class, not a module, so the definition search changes:
grep -rn --include='*.sv' --include='*.svh' \
-E '^[[:space:]]*class[[:space:]]+my_test\b' .And three things are different in practice:
.svh files count. Verification code is routinely split into .svh headers pulled in with `include, so a search restricted to *.sv misses half the environment. That is §4's inclusion mechanism, and it is the norm rather than the exception on the testbench side.
The filelist usually names a package, not the class. One -f entry compiles my_env_pkg.sv, which `includes thirty .svh files. So the intersection in §2 returns nothing for a class, correctly, and the resolution is to find the including package:
grep -rln '`include *"my_test.svh"' --include='*.sv' .And a test is selected at run time, by +UVM_TESTNAME=my_test or equivalent, not at compile time. So "which test ran" is a question about the command line and the log, not about the filelist at all — which is Module 27's subject.
Practice
- Pick a module in a repository you use and run both searches from §1. How many answers does each give?
- Make the definition search precise with the anchored
-Epattern. Does the count change? - Find a module whose file is not named after it. If you cannot, say what that tells you about the project's conventions.
- Run the
comm -12intersection for a module with several definitions. Then runcomm -23and read what is not compiled. - Deliberately feed
communsorted input and describe what you get. - Compare a filelist's path spelling with
grep -r's output. Which prefix has to be stripped? - Run
grep -c '\$'on your real filelists. How many contain variables? - Search your filelists for
-y,+libext,+incdir+and nested-f. Which mechanisms does your flow use? - Write
flatten-filelistand run it on a filelist with a nested-f. Then remove the final newline from the inner file and confirm the last entry still appears. - Add a
+incdir+line to a filelist and confirmflatten-filelistskips it rather than reporting a missing file. - Write
which-moduleand run it for a module that does not exist. Confirm you get exit 1 and not a spurious "1 compiled". - Add a second copy of a module to your filelist and confirm
which-modulereports SEVERAL and exits 1. - Add a
$display("%m from ",FILE)` to a module and confirm the running simulation agrees with your intersection.
Check Your Understanding
- Why is searching for
module <name>better than searching for a file named after it? - What is the authority on which copy of a file is compiled?
- What does
comm -12print, and what doescomm -23print? - Name three reasons the intersection can return nothing when the file is compiled.
- Why is a second, independent confirmation worth the trouble?
- What does
%mexpand to, and what does`__FILE__add? - What are the three mechanisms that compile a file without naming it in a filelist?
- In
flatten-filelist, why do paths resolve against the filelist's directory? - Why does
which-moduleexit 1 when two definitions are compiled? - Why does the class search need
.svh, and why does the filelist intersection fail for a class?
7. Remember
- Search for
module <name>, not for a file named after it. The language does not require the convention. - Anchor the pattern:
^[[:space:]]*module[[:space:]]+name[^a-zA-Z0-9_]— otherwise comments andname_wrappermatch. - Use
[[:space:]], not\s, and prefer[^a-zA-Z0-9_]to\bfor portability. - Three copies is normal: the real one, the vendor's original, and a stale copy in a run directory.
- The filelist decides, and the test is a set intersection:
comm -12of definitions against the filelist. comm -23is the more interesting half — definitions that are present and not compiled.commneeds sorted input, and paths must be spelled identically. Strip./, and resolve withpwd -P.- A
$in a filelist means the simple comparison may lie. Check for it and say so. - Confirm a second way, from the tool:
%mplus`__FILE__. Two independent methods agreeing is evidence; one is a guess. - When they disagree, the tool is right — and you have found an undocumented
-yor`include. - Three invisible-inclusion mechanisms:
-y++libext,`include++incdir+, and nested-f. - Resolve filelist paths against the filelist's own directory.
- Handle a missing final newline (
|| [ -n "$line" ]) and\rfrom Windows. - Two compiled definitions is a defect, not an ambiguity. Report it and exit non-zero.
- For classes, search
.svhtoo, and expect the filelist to name a package rather than the class.
8. What Is Next
Chapter 23.4 takes the other string the log gives you — tb_chip.dut.u_axi — and walks it back to a file and a line. It uses this chapter's answer as its last step, and it is the first thing you do in nearly every debug.
Where this fits
Part of the Linux & Bash curriculum.
