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:
$ /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 hit | Count | Useful? |
|---|---|---|
| Port declaration in the module that owns it | 1 | this is what you wanted |
| Port declaration in a wrapper | 1 | sometimes |
| Named port connection | 1 | yes, for tracing connectivity |
| Comment | 2 | no |
String literal inside $display | 1 | no |
| Logic that uses it | 2 | yes, for behaviour |
| C model parameter and use | 2 | depends 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
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:
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:
$ 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
4Five down to four, and exactly one line went:
$ /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
clkport. (It also containsclk_divandclk_out, which-wignores, 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
clkis 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:
/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:
$ /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:
$ /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
7Three 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:
$ /usr/bin/grep -rnw awvalid --include='*.sv' --include='*.c' rtl tb c_model | wc -l
10Ten — 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:
# 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$ /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
#!/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; }$ ./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 $?
1Six 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
- Pick a short signal name in a repository you use and count the hits for the bare name. Then classify twenty of them.
- Add
-wand record the new count. Which hits went? - Build the nine-line fixture from §3's callout and run
grep -nwon it. Predict which six match before you run it. - Explain why
clk[0]matches-wandclk_divdoes not. - Filter comments with
grep -v '//'and find a line of real code it discarded. - Then use the
:[0-9]+:anchored filter and confirm the count drops but that line survives. - Write a filter without the
:[0-9]+:anchor and confirm it removes nothing. Why is that failure quiet? - Compare
grep -rnw name .withgrep -rnw name rtl tb. How much of the difference is generated directories? - Find a signal's declaration with the port pattern. Then find one it misses, and say why.
- Replace
[^;]*with.*in the port pattern and find a comment it now matches. - Write
sigand run it for a signal declared intbonly. Then use-dto restrict it tortland confirm exit 1. - Run
sigon a name beginning with a dash and confirm the--protects you.
Check Your Understanding
- Why does the bare-name search return mostly noise in a real tree?
- What exactly does
grep -wtreat as a word character? - Why does
$clkmatchgrep -w clk? - What does
-wnot solve at all? - Why is
grep -v '//'wrong in two directions? - What does the
:[0-9]+:prefix do in a second-stage filter, and what happens without it? - Why is two greps the portable answer rather than a negative lookahead?
- Which narrowing stage usually removes the most noise?
- Why does the port-declaration pattern use
[^;]*rather than.*? - In
sig, why must thegrepcapture 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. Soclk[0],(clk),clk;and$clkall match;clk_div,my_clkandclk2do not.-wsolves 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;\bis 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.
