Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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_chip.dut.u_axi is three names separated by dots, and they are not the same kind of name:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ echo "tb_chip.dut.u_axi" | tr '.' '\n' | nl
     1	tb_chip
     2	dut
     3	u_axi
ElementWhat it isHow to find it
tb_chipa module name — the elaboration rootgrep 'module tb_chip'
dutan instance name inside tb_chipgrep the parent's file
u_axian instance name inside dut's modulegrep 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

Walking the hierarchy path tb_chip dot dut dot u_axi back to source. The first element is a module name found by searching for its definition. The second is an instance found inside that module's file, whose type names the next module. The third is an instance inside that module's file. The last step resolves the leaf type to the file the filelist says is compiled.The path from thelogthree names joined bydotsFirst element is amodulethe elaboration rootLater elements areinstanceslabels, not typesEach instance namesa typethe word before the nameSplit on the dotsand strip any generateindexFind its definitionone anchored searchSearch the parentfilenot the whole treeResolve against thefilelistthe leaf may have threecopiessplit12
The top row is the path as the log prints it; the bottom row is what each element resolves to. Only the first element is a module name, which is why the walk goes left to right rather than jumping to the end — each instance can only be found inside the file the previous element led you to.

2. The Walk, Measured

Top-down, one element at a time. Element one is a module definition:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
AXI: elaborated tb_chip.dut.u_axi from rtl/axi/axi_master.sv

Four 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
grep -nE '^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*([[:space:]]*#\(.*\))?[[:space:]]+u_axi[[:space:]]*\(' file.sv

The parameter list spread over several lines, which is the usual formatting for anything with more than two parameters:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
grep -rn --include='*.sv' --include='*.svh' 'create("u_axi"' .
grep -rn --include='*.sv' --include='*.svh' '`uvm_component_utils' . | head

The 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ ./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 $?
0

Eight 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?"

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# 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'
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
grep -rn --include='*.sv' -E '[[:space:]]axi_master([[:space:]]|#)' . |
  grep -vE ':[0-9]+:[[:space:]]*module[[:space:]]'
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
./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

  1. Take a hierarchy path from a log you have and split it with tr '.' '\n' | nl. Which element is a module?
  2. Walk it by hand, top-down, recording the file and line for each element.
  3. Grep for just the last element. How many hits? Could you have chosen correctly without the path?
  4. Find an instance name that appears in two different modules in your repository.
  5. Find a generate block. What do the hierarchy paths of its instances look like, and how many source lines produced them?
  6. Put a hierarchy path containing [0] into a shell variable, use it unquoted, and explain what happens.
  7. Find a parameterised instantiation whose parameter list spans several lines. Confirm that no single-line grep connects the type to the instance name.
  8. Add $display("%m from %s:%0d", FILE, __LINE__) to a frequently instantiated block and read the log.
  9. Write where-is and 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.
  10. Run where-is on a path containing a generate index and confirm the index is stripped.
  11. Change SV=(…) to a plain string and find an option spelling that breaks it.
  12. 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

  1. Why are the elements of a hierarchy path not all the same kind of name?
  2. Which element is a module name, and why that one?
  3. Given an instance name in a file, how do you find its module type?
  4. Why is grepping for the last element alone unreliable?
  5. What does a generate block do to the path-to-line mapping?
  6. Why must a hierarchy path be quoted in a shell script?
  7. Name two instantiation forms where "the preceding word is the type" fails.
  8. Why does grep-based mapping get less useful higher up a UVM testbench?
  9. In where-is, why is SV an array rather than a string?
  10. 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_axi in two modules, disambiguated only by the parent.
  • Generate blocks make the mapping many-to-one: lane[0..3].u_axi all 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.
  • %m with `__FILE__ turns the whole walk into reading a log, for one line in the source.
  • A waveform's $scope records 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 module lines, 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.