Skip to content
VLSI Mentor

Linux & Bash · Part II · Module 24 · Searching Design, Verification and Log Text

Searching a Design Tree for a Signal or Instance

Searching for a signal name is the most frequent thing an engineer does in a repository, and the naive version returns mostly noise — because short HDL names are substrings of longer ones and appear in comments and strings as often as in logic. This chapter narrows a search in four stages, each measured.

Intermediate21 min readLinuxPart IIgrepRTLSearch

Part II · Module 24 · Chapter 24.1 · Searching a Design Tree for a Signal or Instance

Module 23 found files. This module finds lines, and the two problems are different enough to need their own strategies — because what you are searching is either text a person wrote or text a tool wrote, and those behave nothing alike.

This chapter is the first kind: source. It opens with the search everybody runs and the reason it is almost useless.

1. Ten Hits, One Declaration

awvalid in chip_a, across every language in the tree:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ /usr/bin/grep -rn awvalid --include='*.sv' --include='*.svh' --include='*.c' --include='*.py' .
tb/agents/axi_mon.sv:2:module axi_mon (input logic clk, input logic reset_n, input logic awvalid);
tb/agents/axi_mon.sv:6:    .awvalid  (awvalid),
tb/agents/axi_mon.sv:10:  // monitor awvalid for protocol checks
tb/agents/axi_mon.sv:11:  always @(posedge clk) if (awvalid) $display("MON: awvalid high");
c_model/ref_model.c:4:uint32_t step(uint32_t awaddr, int awvalid) {
c_model/ref_model.c:5:  if (!reset_n) return 0;      /* awvalid ignored during reset */
c_model/ref_model.c:6:  return awvalid ? awaddr : 0;
rtl/axi/axi_slave.sv:4:                   input logic awvalid, output logic awready,
rtl/axi/axi_slave.sv:6:  // awvalid is held until awready; see the spec
rtl/axi/axi_slave.sv:9:    else          awready <= awvalid;

Ten lines. Classify them:

Kind of hitCountUseful?
Port declaration in the module that owns it1this is what you wanted
Port declaration in a wrapper1sometimes
Named port connection1yes, for tracing connectivity
Comment2no
String literal inside $display1no
Logic that uses it2yes, for behaviour
C model parameter and use2depends on the question

Three of ten are noise, and that ratio gets dramatically worse with scale: a real tree returns four hundred hits for a signal that is declared once, and the comments and log strings dominate because engineers mention signal names in prose constantly.

Note also that the ten hits came from four different languages, which is Chapter 24.3's subject. And the /usr/bin/grep is deliberate, for Chapter 23.5 §3's measured reason.

2. The Narrowing Sequence

Four stages of narrowing a source search

Four stages of narrowing a source search. One, search the bare name, which matches substrings, comments and strings. Two, require a whole word, which removes substring matches. Three, anchor on an HDL construct, which removes comments and strings. Four, scope the search to the directories that matter. The result is an answer rather than a dump.From a name to an answer01THE BARE NAMEmatches substrings, comments and strings02REQUIRE A WHOLE WORDgrep -w — removes clk_div and my_clk03ANCHOR ON A CONSTRUCTa declaration, not a mention04SCOPE THE DIRECTORIESyour design, not the whole farm05ASK FOR A SHAPEa count, a list, a table
Each stage removes a different kind of noise, and they compose: whole-word matching removes substring hits, construct anchoring removes comments and strings, scoping removes other people's code and generated copies, and asking for the right shape of answer removes the rest. Running only the first stage is what produces a four-hundred-line result nobody reads.

Stages 2 and 4 are this chapter; stage 3 is Chapter 24.2 and stage 5 is Chapter 24.6.

3. grep -w, and What It Actually Considers a Word

The registry's suggested command, and the first real improvement:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
grep -rnw 'awvalid' --include='*.sv' rtl/

-w requires the match to be a whole word. Measured on a file written to contain one awkward case per line:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ cat -n rtl/core/clk_div.sv
     1	module clk_div (input clk, output clk_out);
     2	  // the clk comes from the pad
     3	  wire clkx;
     4	  assign clk_out = clk;  // clock
     5	  initial $display("clk seen");
     6	endmodule
$ /usr/bin/grep -c clk rtl/core/clk_div.sv
5
$ /usr/bin/grep -cw clk rtl/core/clk_div.sv
4

Five down to four, and exactly one line went:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ /usr/bin/grep -nw clk rtl/core/clk_div.sv
1:module clk_div (input clk, output clk_out);
2:  // the clk comes from the pad
4:  assign clk_out = clk;  // clock
5:  initial $display("clk seen");

Line 3 — wire clkx; — is gone, because clkx is not a whole word. That is the entire benefit, and it is worth being precise about what survived:

  • Line 1 stays, and correctly: it contains the standalone clk port. (It also contains clk_div and clk_out, which -w ignores, but one whole-word match is enough to keep the line.)
  • Line 2 stays — it is a comment.
  • Line 4 stays — real logic, with a trailing comment.
  • Line 5 stays — the clk is inside a string literal in $display.

So -w removed the one substring hit and kept both the comment and the string. It solves the substring problem completely and the context problem not at all, which is the single most useful thing to know about it.

4. Excluding Comments and Strings — and Why It Is a Trap

The obvious next move is to filter out comment lines:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
/usr/bin/grep -rnw awvalid --include='*.sv' . | /usr/bin/grep -v '//'

This is wrong in both directions, and both failures are common:

It drops real code that has a trailing comment. assign awready = awvalid; // handshake is logic, and this discards it. In well-commented RTL that is a large fraction of the useful lines.

And it keeps code inside block comments. A /* ... */ region spanning twenty lines has no // on any of them.

The honest version filters only lines whose first non-blank character begins a comment:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ /usr/bin/grep -rnw awvalid --include='*.sv' . | /usr/bin/grep -vE ':[0-9]+:[[:space:]]*(//|\*|/\*)'
tb/agents/axi_mon.sv:2:module axi_mon (input logic clk, input logic reset_n, input logic awvalid);
tb/agents/axi_mon.sv:6:    .awvalid  (awvalid),
tb/agents/axi_mon.sv:11:  always @(posedge clk) if (awvalid) $display("MON: awvalid high");
rtl/axi/axi_slave.sv:4:                   input logic awvalid, output logic awready,
rtl/axi/axi_slave.sv:9:    else          awready <= awvalid;

Five lines instead of ten, and the two whole-line comments are gone. The :[0-9]+: prefix in the pattern is what makes it work — it anchors to just after grep's own file:line: prefix, so [[:space:]]*// means "the line's own text starts with a comment". Without that anchor, grep -vE '^[[:space:]]*//' would never match anything, because every line starts with a filename.

The string literal on line 11 is still there, and no line-based filter can remove it without also removing the always block it sits in. That is the structural limit of line-oriented search, and the reason Chapter 24.2 stops filtering output and starts matching constructs instead.

5. Scoping: the Cheapest Noise Reduction

Stage 4, and it usually removes more than stages 2 and 3 together. The registry's hint ends in rtl/ for exactly this reason:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ /usr/bin/grep -rnw awvalid --include='*.sv' . | wc -l
7
$ /usr/bin/grep -rnw awvalid --include='*.sv' rtl | wc -l
3
$ /usr/bin/grep -rnw awvalid --include='*.sv' tb | wc -l
4
$ /usr/bin/grep -rnw awvalid --include='*.sv' rtl tb | wc -l
7

Three questions, three scopes:

"Where is this declared?" — rtl only, which is three lines instead of seven, because a declaration in tb is a testbench port rather than the design's.

"Who drives this?" — rtl and tb, because the driver may be either. Here that is the same seven as ., since this repository happens to keep all its SystemVerilog in those two trees; in a real one it is a large reduction.

"Does the C model agree?" — add c_model and the extension, which is Chapter 24.3:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ /usr/bin/grep -rnw awvalid --include='*.sv' --include='*.c' rtl tb c_model | wc -l
10

Ten — the full set from §1, reached deliberately rather than by searching everything.

And the scope that is always wrong is . in a repository with run directories, for Chapter 23.5's measured reason. Naming the directories you want is both faster and safer than excluding the ones you do not.

6. Finding a Declaration Specifically

The question behind most signal searches is "what is this thing, and how wide is it", which is a search for the declaration rather than for the name.

Three declaration shapes in SystemVerilog, and a pattern for each:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# a port declaration: a direction keyword somewhere before the name
/usr/bin/grep -rnE '(input|output|inout)[^;]*\bawvalid\b' --include='*.sv' rtl

# an internal signal: a type keyword then the name
/usr/bin/grep -rnE '^[[:space:]]*(wire|reg|logic|bit|byte|int)([[:space:]]*\[[^]]*\])?[[:space:]]+[^;]*\bawvalid\b' --include='*.sv' rtl

# anything at all that ends in a semicolon and mentions it — the fallback
/usr/bin/grep -rnE '\bawvalid\b[^;]*;' --include='*.sv' rtl
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ /usr/bin/grep -rnE '(input|output|inout)[^;]*\bawvalid\b' --include='*.sv' rtl
rtl/axi/axi_slave.sv:4:                   input logic awvalid, output logic awready,

One line, and it is the declaration. From ten hits to one, by asking a different question rather than by filtering harder. Widened to rtl tb it gives two — the design's port and the testbench wrapper's — which is the right answer to a slightly different question.

Two things about that pattern are deliberate:

[^;]* between the keyword and the name allows input logic [31:0] awvalid and input wire awvalid while refusing to cross a statement boundary. Without the [^;]* restriction a greedy .* would match output logic awready; // awvalid is the input — a comment on a different declaration.

And \b around the name rather than -w, because the pattern already has other content and -w applies to the whole match. \b is a GNU extension that BSD grep accepts; the strictly portable spelling is [^a-zA-Z0-9_], which costs legibility and is worth it in a script someone else will run.

7. The Script

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/usr/bin/env bash
# sig — where is a signal declared, connected and used?
#
# usage: sig [-d dir]... <name>
# exit:  0 found · 1 not found · 2 bad usage
set -o nounset

GREP=${GREP:-/usr/bin/grep}
EXT=(--include='*.sv' --include='*.svh' --include='*.v' --include='*.vh')
dirs=()

while getopts ':d:' o; do
  case $o in
    d) dirs+=("$OPTARG") ;;
    :) printf 'sig: -d needs a directory\n' >&2; exit 2 ;;
    \?) printf 'sig: unknown option -%s\n' "$OPTARG" >&2; exit 2 ;;
  esac
done
shift $(( OPTIND - 1 ))

[ $# -eq 1 ] || { printf 'usage: sig [-d dir]... <name>\n' >&2; exit 2; }
name=$1

if [ "${#dirs[@]}" -eq 0 ]; then
  for d in rtl tb src hdl; do [ -d "$d" ] && dirs+=("$d"); done
fi
[ "${#dirs[@]}" -gt 0 ] || { printf 'sig: no source directories; use -d\n' >&2; exit 2; }

W="[^a-zA-Z0-9_]"            # portable word boundary
NOCOMMENT=':[0-9]+:[[:space:]]*(//|\*|/\*)'
found=0

show() {                     # $1 = heading, $2 = pattern
  local out
  out=$("$GREP" -rnE "${EXT[@]}" -- "$2" "${dirs[@]}" 2>/dev/null |
        "$GREP" -vE "$NOCOMMENT") || return 0
  [ -n "$out" ] || return 0
  printf -- '-- %s\n' "$1"
  printf '%s\n' "$out" | sed 's/^/   /'
  printf '\n'
  found=1
}

show "declared as a port"  "(input|output|inout)[^;]*(^|$W)${name}($W|\$)"
show "declared as a signal" "^[[:space:]]*(wire|reg|logic|bit)([[:space:]]*\[[^]]*\])?[[:space:]]+[^;]*(^|$W)${name}($W|\$)"
show "connected to a port"  "^[[:space:]]*\.[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*\([[:space:]]*${name}[[:space:]]*\)"
show "used in logic"        "(^|$W)${name}($W|\$)"

[ "$found" -eq 1 ] || { printf 'sig: %s not found in %s\n' "$name" "${dirs[*]}" >&2; exit 1; }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ ./sig awvalid
-- declared as a port
   rtl/axi/axi_slave.sv:4:                   input logic awvalid, output logic awready,
   tb/agents/axi_mon.sv:2:module axi_mon (input logic clk, input logic reset_n, input logic awvalid);

-- connected to a port
   tb/agents/axi_mon.sv:6:    .awvalid  (awvalid),

-- used in logic
   rtl/axi/axi_slave.sv:4:                   input logic awvalid, output logic awready,
   rtl/axi/axi_slave.sv:9:    else          awready <= awvalid;
   tb/agents/axi_mon.sv:2:module axi_mon (input logic clk, input logic reset_n, input logic awvalid);
   tb/agents/axi_mon.sv:6:    .awvalid  (awvalid),
   tb/agents/axi_mon.sv:11:  always @(posedge clk) if (awvalid) $display("MON: awvalid high");

$ echo $?
0
$ ./sig no_such_signal
sig: no_such_signal not found in rtl tb
$ echo $?
1

Six decisions:

Sections, not one list. "Declared as a port" is a different answer from "used in logic", and a reader wants the first one first. The last section deliberately repeats lines from the earlier ones, because "every mention" is also a question people have.

(^|$W)name($W|\$) as the word boundary, spelled out rather than using \b, so the pattern is POSIX ERE and runs under any grep. The \$ is escaped because it is inside a double-quoted shell string that must reach grep as a literal $ anchor.

GREP pinned, for Chapter 23.5 §3's reason — a search tool that silently skips .gitignored files makes this script behave differently in a batch job.

|| return 0 on the capture, because grep exits 1 when it finds nothing and the function must not abort the script. Chapter 11.1's status, and a case where discarding it is correct.

-- "$2" before the pattern, so a signal name beginning with a dash is a pattern rather than an option. Chapter 1.5.

And a found flag giving exit 1, so the script is usable in a conditional — sig x || echo "not in this design". Chapter 21.4.

Practice

  1. Pick a short signal name in a repository you use and count the hits for the bare name. Then classify twenty of them.
  2. Add -w and record the new count. Which hits went?
  3. Build the nine-line fixture from §3's callout and run grep -nw on it. Predict which six match before you run it.
  4. Explain why clk[0] matches -w and clk_div does not.
  5. Filter comments with grep -v '//' and find a line of real code it discarded.
  6. Then use the :[0-9]+: anchored filter and confirm the count drops but that line survives.
  7. Write a filter without the :[0-9]+: anchor and confirm it removes nothing. Why is that failure quiet?
  8. Compare grep -rnw name . with grep -rnw name rtl tb. How much of the difference is generated directories?
  9. Find a signal's declaration with the port pattern. Then find one it misses, and say why.
  10. Replace [^;]* with .* in the port pattern and find a comment it now matches.
  11. Write sig and run it for a signal declared in tb only. Then use -d to restrict it to rtl and confirm exit 1.
  12. Run sig on a name beginning with a dash and confirm the -- protects you.

Check Your Understanding

  1. Why does the bare-name search return mostly noise in a real tree?
  2. What exactly does grep -w treat as a word character?
  3. Why does $clk match grep -w clk?
  4. What does -w not solve at all?
  5. Why is grep -v '//' wrong in two directions?
  6. What does the :[0-9]+: prefix do in a second-stage filter, and what happens without it?
  7. Why is two greps the portable answer rather than a negative lookahead?
  8. Which narrowing stage usually removes the most noise?
  9. Why does the port-declaration pattern use [^;]* rather than .*?
  10. In sig, why must the grep capture end in || return 0?

Chapter 29.8 is where these searches become a report, and where a comparison that looked correct reported no change while two runs had flipped in opposite directions.

8. Remember

  • A bare-name search is mostly noise. Measured: ten hits for awvalid, one of them the declaration.
  • Narrow in four stages: whole word, construct anchor, scope, then ask for a shape.
  • -w's word characters are letters, digits and underscore only. So clk[0], (clk), clk; and $clk all match; clk_div, my_clk and clk2 do not.
  • -w solves substrings and nothing else. It cannot see comments or strings.
  • grep -v '//' drops real code with trailing comments and keeps block comments.
  • Anchor a second-stage filter with :[0-9]+:, or it silently removes nothing.
  • Two greps is the portable spelling — BSD grep has no -P.
  • Scoping removes the most noise. Name rtl tb, never ..
  • Search for the declaration, not the name: a direction keyword plus [^;]* plus the name gave one line out of ten.
  • Use [^;]*, not .*, so a pattern cannot cross into a neighbouring statement's comment.
  • Spell the word boundary [^a-zA-Z0-9_] in anything portable; \b is an extension.
  • Pin the grep in a script, and put -- before the pattern.
  • A filter that does not change the count is not working. Check it.

9. What Is Next

Chapter 24.2 replaces filtering with matching: patterns that describe HDL constructs — module headers, instantiations, port connections, parameter overrides — including the measured reason one of the obvious instantiation patterns silently misses every parameterised instance in the tree.

Where this fits

Part of the Linux & Bash curriculum.