Linux & Bash · Part II · Module 23 · Navigating RTL and Verification Repositories
Mapping a Hierarchy Path Back to Files
Every error a simulator reports names a place in the design hierarchy rather than a place in your source. Turning one into the other is the first move of nearly every debug, it is pure shell, and it has one subtlety that makes the obvious approach wrong.
Intermediate21 min readLinuxPart IIDebuggingHierarchygrep
Part II · Module 23 · Chapter 23.4 · Mapping a Hierarchy Path Back to Files
A simulator speaks in hierarchy paths. You work in files. Every debugging session starts by translating one into the other, and most people do it by guessing which file sounds right.
1. What a Hierarchy Path Actually Is
From a real run of chip_a:
$ 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_chiptb_chip.dut.u_axi is three names separated by dots, and they are not the same kind of name:
$ echo "tb_chip.dut.u_axi" | tr '.' '\n' | nl
1 tb_chip
2 dut
3 u_axi| Element | What it is | How to find it |
|---|---|---|
tb_chip | a module name — the elaboration root | grep 'module tb_chip' |
dut | an instance name inside tb_chip | grep the parent's file |
u_axi | an instance name inside dut's module | grep that module's file |
The first element is a module; every later element is an instance. That asymmetry is the entire technique, and ignoring it is why the obvious approach — grepping for the last element — gives wrong answers.
The reason for the asymmetry is simple once stated: the root was named on the command line (or chosen by the tool as the module nobody instantiates), so it is a type. Everything below it was named by whoever wrote the instantiation, so those are labels. A label means nothing without knowing which module it is a label inside.
Walking a hierarchy path back to source, one element at a time
2. The Walk, Measured
Top-down, one element at a time. Element one is a module definition:
$ grep -rn --include='*.sv' -E '^[[:space:]]*module[[:space:]]+tb_chip[^a-zA-Z0-9_]' .
./tb/tests/tb_chip.sv:1:module tb_chip;Element two, dut, is an instance inside that file:
$ grep -nE '[[:space:]]dut[[:space:]]*\(' tb/tests/tb_chip.sv
4: chip_top dut (.clk(clk), .rst_n(rst_n), .status(status));The word before the instance name is the module type. So dut is a chip_top, and element three lives in chip_top's file:
$ grep -rn --include='*.sv' -E '^[[:space:]]*module[[:space:]]+chip_top[^a-zA-Z0-9_]' .
./rtl/core/chip_top.sv:2:module chip_top (input logic clk, input logic rst_n, output logic [7:0] status);
$ grep -nE '[[:space:]]u_axi[[:space:]]*\(' rtl/core/chip_top.sv
4: axi_master u_axi (.clk(clk), .rst_n(rst_n), .stat(s_axi));u_axi is an axi_master, and Chapter 23.3's intersection says which of the three axi_master.sv files that is: rtl/axi/axi_master.sv.
Which the running simulation independently confirms, because the design prints its own file name:
AXI: elaborated tb_chip.dut.u_axi from rtl/axi/axi_master.svFour greps and one cross-reference, from a string in a log to a file and a line number — and a second method agreeing.
3. The Instance Name Alone Is Not Unique
The reason to walk top-down rather than jumping to the last element. Add a second module that also instantiates an axi_master called u_axi — which is entirely legal and extremely common, because u_axi is a natural name:
$ grep -rn --include='*.sv' -E '[[:space:]]u_axi[[:space:]]*\(' .
./rtl/core/chip_top.sv:4: axi_master u_axi (.clk(clk), .rst_n(rst_n), .stat(s_axi));
./rtl/uart/uart_tx.sv:8: axi_master u_axi (.clk(clk), .rst_n(1'b1), .stat(stat));Two hits, and the log's path points at exactly one of them. Nothing in the string u_axi distinguishes them; the discriminator is the parent, which is chip_top for the first and other_block for the second — and the full path tb_chip.dut.u_axi tells you the parent is dut's module type.
So the rule is: grep for the last element to get candidates, and use the path's second-to-last element to choose between them. Jumping straight to the last element works right up until it does not, and when it fails it fails silently — you read a module that exists, is instantiated, and is not the one in your log.
4. When the Word Before the Name Is Not the Type
Three real forms where "take the preceding word" breaks, all of which appear in production RTL:
A parameterised instantiation.
axi_master #(.ADDR_W(32), .DATA_W(64)) u_axi (.clk(clk), ...);The type is still the first word, but it is no longer adjacent to the instance name. A pattern that captures it:
grep -nE '^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*([[:space:]]*#\(.*\))?[[:space:]]+u_axi[[:space:]]*\(' file.svThe parameter list spread over several lines, which is the usual formatting for anything with more than two parameters:
axi_master #(
.ADDR_W (32),
.DATA_W (64)
) u_axi (
.clk (clk),Here the type and the instance name are not on the same line at all, and no single-line regex can connect them. This is the case where grep stops being the right tool and the answer is either awk with state, or — far better — asking the tool.
And a UVM component, where there is no instantiation line to find. A uvm_component is created at run time by type_id::create("u_axi", this), so the hierarchy path exists only during the run:
grep -rn --include='*.sv' --include='*.svh' 'create("u_axi"' .
grep -rn --include='*.sv' --include='*.svh' '`uvm_component_utils' . | headThe path's elements then correspond to the string literals passed to create, not to any declared identifier — which is why grep-based hierarchy mapping gets steadily less useful the further up the testbench you go, and why UVM's own uvm_top.print_topology() exists.
5. The Script
The walk from §2, automated. The first version of this script had a bug worth keeping, so it is shown second.
#!/usr/bin/env bash
# where-is — map a hierarchy path to the file and line of each element
#
# usage: where-is <hierarchy.path> [-f filelist] [-C dir]
# exit: 0 resolved · 1 an element could not be resolved · 2 bad usage
set -o nounset
usage() { printf 'usage: where-is <hierarchy.path> [-f filelist] [-C dir]\n' >&2; exit 2; }
[ $# -ge 1 ] || usage
path=$1; shift
fl=''; root='.'
while getopts ':f:C:' opt; do
case $opt in
f) fl=$OPTARG ;;
C) root=$OPTARG ;;
:) printf 'where-is: -%s needs a value\n' "$OPTARG" >&2; exit 2 ;;
\?) printf 'where-is: unknown option -%s\n' "$OPTARG" >&2; usage ;;
esac
done
[ -d "$root" ] || { printf 'where-is: no such directory: %s\n' "$root" >&2; exit 2; }
case $fl in '') : ;; /*) : ;; *) fl=$(pwd -P)/$fl ;; esac # before the cd
cd -- "$root" || exit 2
[ -z "$fl" ] || [ -r "$fl" ] || { printf 'where-is: cannot read filelist: %s\n' "$fl" >&2; exit 2; }
GREP=${GREP:-/usr/bin/grep}
SV=(--include='*.sv' --include='*.v' --include='*.svh')
clean=$(printf '%s' "$path" | sed 's/\[[0-9]*\]//g') # strip generate indices
IFS='.' read -r -a elems <<< "$clean"
find_module() { # $1 = module name -> one file path
local defs hit n
defs=$("$GREP" -rl "${SV[@]}" -E "^[[:space:]]*module[[:space:]]+$1[^a-zA-Z0-9_]" . 2>/dev/null |
sed 's|^\./||' | sort)
[ -n "$defs" ] || return 1
if [ -n "$fl" ]; then
hit=$(comm -12 <(printf '%s\n' "$defs") <(sed 's|^\./||' "$fl" | sort))
if [ -n "$hit" ]; then printf '%s\n' "$hit" | head -1; return 0; fi
printf '%s\n' "$defs" | head -1
printf 'where-is: %s: no definition is in the filelist; guessing\n' "$1" >&2
return 0
fi
n=$(printf '%s\n' "$defs" | grep -c .)
[ "$n" -eq 1 ] || printf 'where-is: %s has %s definitions and no filelist given\n' "$1" "$n" >&2
printf '%s\n' "$defs" | head -1
}
mod=${elems[0]}
file=$(find_module "$mod") || { printf 'where-is: cannot find module %s\n' "$mod" >&2; exit 1; }
printf '%-22s module %s\n' "$mod" "$file"
i=1
while [ "$i" -lt "${#elems[@]}" ]; do
inst=${elems[$i]}
line=$("$GREP" -nE "[[:space:]]$inst[[:space:]]*\(" "$file" 2>/dev/null | head -1)
if [ -z "$line" ]; then
printf 'where-is: %s not found in %s\n' "$inst" "$file" >&2
printf ' (generate block, parameterised instance, or a UVM create)\n' >&2
exit 1
fi
lineno=${line%%:*}; text=${line#*:}
type=$(printf '%s' "$text" | sed -E 's/^[[:space:]]*//; s/[[:space:]]*#?\(.*//; s/[[:space:]].*//')
printf '%-22s instance %s:%s type %s\n' "$inst" "$file" "$lineno" "$type"
file=$(find_module "$type") || { printf 'where-is: cannot find module %s\n' "$type" >&2; exit 1; }
i=$(( i + 1 ))
done
printf '\nleaf module source: %s\n' "$file"With the filelist:
$ ./where-is tb_chip.dut.u_axi -f filelists/chip.f
tb_chip module tb/tests/tb_chip.sv
dut instance tb/tests/tb_chip.sv:4 type chip_top
u_axi instance rtl/core/chip_top.sv:4 type axi_master
leaf module source: rtl/axi/axi_master.sv
$ echo $?
0Eight decisions, each a Part I chapter arriving where it is needed:
[ $# -ge 1 ] || usage rather than ${1:?message}. The :? form is shorter and its exit status is not dependable: measured here, a :? failure gave 1 inside a script and 127 under bash -c. When the status is part of a documented contract — Chapter 21.4 — check explicitly and exit 2 yourself.
The filelist path is made absolute before the cd. A relative -f filelists/chip.f means relative to where the user is standing, and the script then changes directory. Resolving after the cd is a bug that only shows up when -C is used, which is the worst kind.
GREP=${GREP:-/usr/bin/grep} — the grep is pinned, and overridable. Chapter 22.6 §2 measured why: on this machine grep is a shell function dispatching to ugrep, which silently skips .gitignored directories, so the same script would find two definitions interactively and three in a batch job. Chapter 23.5 measures that divergence properly.
sed 's/\[[0-9]*\]//g' strips generate indices first, so tb_chip.dut.lane[2].u_axi resolves to the one source line that produced all four lanes — §3's many-to-one.
IFS='.' read -r -a elems <<< "$clean" splits on dots into an array, with IFS set for one command so it cannot leak — Chapter 9.2 §6 — and -a building the array in one step, which is Chapter 19.2.
SV=(--include=… ) as an array, expanded "${SV[@]}". A string of options would be word-split and would break on any option containing a space; an array passes each element as exactly one argument — Chapter 19.5.
The type is extracted by stripping, not matching: leading space, then everything from #( or ( onward, then everything after the first word. Three substitutions in one sed rather than three processes, and it handles §4's parameterised form.
And ${elems[$i]}, not ${elems[-1]}, because this machine's Bash is 3.2 and negative subscripts arrived in 4.3 — Chapter 21.6 §2.
6. Going the Other Way
Occasionally you have a file and want to know where its module ends up in the hierarchy — "is this block even instantiated?"
# 1. what modules does this file define?
grep -oE '^[[:space:]]*module[[:space:]]+[a-zA-Z_][a-zA-Z0-9_]*' rtl/axi/axi_master.sv |
awk '{print $NF}'
# 2. who instantiates them?
grep -rn --include='*.sv' -E '[[:space:]]axi_master([[:space:]]|#)' . | grep -v '^\./rtl/axi/axi_master.sv'$ grep -oE '^[[:space:]]*module[[:space:]]+[a-zA-Z_][a-zA-Z0-9_]*' rtl/axi/axi_master.sv | awk '{print $NF}'
axi_master
$ grep -rn --include='*.sv' -E '[[:space:]]axi_master([[:space:]]|#)' . | grep -v 'rtl/axi/axi_master.sv'
./rtl/core/chip_top.sv:4: axi_master u_axi (.clk(clk), .rst_n(rst_n), .stat(s_axi));
./ip/vendor_axi/rtl/axi_master.sv:2:module axi_master (input logic clk, input logic rst_n, output logic [7:0] stat);Two hits, and the second is a false positive — it is the vendor copy's own module line, matched because module axi_master contains a space followed by axi_master. The fix is to exclude definition lines explicitly:
grep -rn --include='*.sv' -E '[[:space:]]axi_master([[:space:]]|#)' . |
grep -vE ':[0-9]+:[[:space:]]*module[[:space:]]'./rtl/core/chip_top.sv:4: axi_master u_axi (.clk(clk), .rst_n(rst_n), .stat(s_axi));One instantiation, which is the answer. A module that nothing instantiates is either a top level, dead code, or selected at run time — and distinguishing those three is worth doing before deleting anything.
Practice
- Take a hierarchy path from a log you have and split it with
tr '.' '\n' | nl. Which element is a module? - Walk it by hand, top-down, recording the file and line for each element.
- Grep for just the last element. How many hits? Could you have chosen correctly without the path?
- Find an instance name that appears in two different modules in your repository.
- Find a generate block. What do the hierarchy paths of its instances look like, and how many source lines produced them?
- Put a hierarchy path containing
[0]into a shell variable, use it unquoted, and explain what happens. - Find a parameterised instantiation whose parameter list spans several lines. Confirm that no single-line grep connects the type to the instance name.
- Add
$display("%m from %s:%0d",FILE,__LINE__)to a frequently instantiated block and read the log. - Write
where-isand run it on a three-element path. Then run it on a path whose middle element does not exist and confirm you get exit 1 with the hint. - Run
where-ison a path containing a generate index and confirm the index is stripped. - Change
SV=(…)to a plain string and find an option spelling that breaks it. - Use §6's reverse search on a module you suspect is dead. Is it a top level, dead, or run-time selected?
Check Your Understanding
- Why are the elements of a hierarchy path not all the same kind of name?
- Which element is a module name, and why that one?
- Given an instance name in a file, how do you find its module type?
- Why is grepping for the last element alone unreliable?
- What does a generate block do to the path-to-line mapping?
- Why must a hierarchy path be quoted in a shell script?
- Name two instantiation forms where "the preceding word is the type" fails.
- Why does
grep-based mapping get less useful higher up a UVM testbench? - In
where-is, why isSVan array rather than a string? - In §6, why did the reverse search return the vendor file, and how is that excluded?
7. Remember
- A hierarchy path's first element is a module; every later element is an instance. That asymmetry is the technique.
- Walk top-down, resolving each element in the previous module's file.
- The word before an instance name is its module type — or the word before
#(. - Instance names are not unique. Measured:
u_axiin two modules, disambiguated only by the parent. - Generate blocks make the mapping many-to-one:
lane[0..3].u_axiall come from one line, and the index says which one failed. - Quote every hierarchy path in a script —
lane[0]is a glob. - Multi-line parameter lists defeat any single-line grep. That is where you stop inferring and ask the tool.
- UVM paths are run-time strings from
type_id::create, not declared identifiers. %mwith`__FILE__turns the whole walk into reading a log, for one line in the source.- A waveform's
$scoperecords are the most reliable hierarchy because they are what actually elaborated. - Pass grep options as an array, not a string.
- Use
${a[$i]}, not${a[-1]}— negative subscripts need Bash 4.3 and this is 3.2. - In a reverse search, exclude
modulelines, or a definition matches your instantiation pattern. - A module nothing instantiates is a top level, dead, or run-time selected — check which before deleting.
8. What Is Next
Every search in this chapter was run against the whole tree, which worked because chip_a is small. Chapter 23.5 measures what happens when it is not: generated directories contain copies of your source, and a search that includes them describes the last simulation rather than the design.
Where this fits
Part of the Linux & Bash curriculum.
