Linux & Bash · Part II · Module 24 · Searching Design, Verification and Log Text
Patterns for HDL Source
Chapter 24.1 narrowed a search by filtering its output. This chapter stops filtering and starts describing: patterns that match a module header, an instantiation, a port connection or a parameter override, so that a comment mentioning the name simply does not match. One of them has a defect that costs you every parameterised instance in the tree.
Intermediate21 min readLinuxPart IIRegexRTLgrep
Part II · Module 24 · Chapter 24.2 · Patterns for HDL Source
A signal name is a string. A declaration is a shape — a direction keyword, some type words, the name, a semicolon. Searching for the shape instead of the string removes comments and strings for free, because a comment does not have the shape.
Four shapes cover almost everything you will search for.
1. Module Headers
The simplest, and the one every other pattern is built from:
$ /usr/bin/grep -rnE '^[[:space:]]*module[[:space:]]+[a-zA-Z_][a-zA-Z0-9_]*' --include='*.sv' rtl tb
rtl/core/chip_top.sv:2:module chip_top (input logic clk, input logic rst_n, output logic [7:0] status);
rtl/core/clk_div.sv:1:module clk_div (input clk, output clk_out);
rtl/axi/axi_slave.sv:2:module axi_slave #(parameter ADDR_W = `ADDR_W, parameter DATA_W = `DATA_W)
rtl/axi/axi_master.sv:2:module axi_master (input logic clk, input logic rst_n, output logic [7:0] stat);
rtl/uart/uart_tx.sv:1:module uart_tx (input logic clk, input logic rst_n, output logic [7:0] stat);
tb/tests/tb_regress.sv:2:module tb_regress;
tb/tests/tb_chip.sv:1:module tb_chip;
tb/agents/axi_mon.sv:2:module axi_mon (input logic clk, input logic reset_n, input logic awvalid);Eight modules, one line each. Four details in that pattern are load-bearing:
^[[:space:]]* — anchored to the start of a line, which is what excludes the word "module" appearing in a comment mid-line. Indentation is allowed because a module inside a `ifdef is often indented.
module[[:space:]]+ requires at least one space, so modulename does not match.
[a-zA-Z_][a-zA-Z0-9_]* is an identifier: a letter or underscore, then letters, digits or underscores. Writing it out rather than using \w keeps the pattern POSIX ERE, which matters because Chapter 21.6 is permanently in play.
And -E, because +, ? and | in basic regex need backslashes. Every pattern in this chapter is ERE; Chapter 14.3 is the distinction.
Note the three variations the pattern handled without being told to: a header with no port list (module tb_regress;), one whose parameter list follows on the same line, and one indented by nothing. A pattern that requires a ( after the name would have missed two of the eight.
2. Instantiations — and the Defect in the Obvious Pattern
Three instantiation patterns, and what each one costs
An instantiation is a type name, optional parameter overrides, an instance name, and an open parenthesis. Written directly:
$ /usr/bin/grep -rnE '^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*([[:space:]]*#\([^)]*\))?[[:space:]]+u_[a-zA-Z0-9_]*[[:space:]]*\(' --include='*.sv' rtl tb
rtl/core/chip_top.sv:4: axi_master u_axi (.clk(clk), .rst_n(rst_n), .stat(s_axi));
rtl/core/chip_top.sv:5: uart_tx u_uart (.clk(clk), .rst_n(rst_n), .stat(s_uart));Two hits. There are three instantiations in that scope, and the missing one is this:
axi_slave #(.ADDR_W(16), .DATA_W(32)) u_slv (3. The Instance-Name Convention, and What It Costs
Both patterns above require the instance name to start with u_. That is a strong convention and it is not universal. Dropping it:
$ /usr/bin/grep -rnE '^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*([[:space:]]*#\(.*\))?[[:space:]]+[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*\(' --include='*.sv' . | wc -l
11Eleven hits, up from three. The extra eight are not all instantiations — five of them are module headers, because module is an identifier, the module name is another identifier, and a port list opens with (. The pattern cannot tell module axi_mon ( from axi_slave u_slv (.
The fix is to exclude lines whose first word is a SystemVerilog keyword, which needs a second grep because BSD has no negative lookahead:
$ /usr/bin/grep -P x /dev/null
grep: invalid option -- P$ /usr/bin/grep -rnE '^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*([[:space:]]*#\(.*\))?[[:space:]]+[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*\(' --include='*.sv' . |
> /usr/bin/grep -vE ':[0-9]+:[[:space:]]*(module|endmodule|function|task|interface|package|class|program|if|else|case|for|while|always|always_ff|always_comb|initial|assign|return)[^a-zA-Z0-9_]'
./tb/tests/tb_chip.sv:4: chip_top dut (.clk(clk), .rst_n(rst_n), .status(status));
./tb/agents/axi_mon.sv:3: axi_slave #(.ADDR_W(16), .DATA_W(32)) u_slv (
./rtl/core/chip_top.sv:4: axi_master u_axi (.clk(clk), .rst_n(rst_n), .stat(s_axi));
./rtl/core/chip_top.sv:5: uart_tx u_uart (.clk(clk), .rst_n(rst_n), .stat(s_uart));Four, and all four are instantiations — including chip_top dut (, which the u_ patterns both missed because dut does not start with u_.
So there are two defensible positions, and the choice is about which error you prefer:
| Pattern | Hits | Misses | Spurious |
|---|---|---|---|
Requires u_ | 3 | instances named dut, slv0, inst_x | none |
| Any identifier, keywords filtered | 4 | none in this tree | possible, in unusual code |
The second is the better default, and the keyword list is the price. Note also the :[0-9]+: anchor in the filter — Chapter 24.1 §4's rule, without which the filter removes nothing and you believe it worked.
4. Port Connections and Parameter Overrides
Two shapes that are easier than instantiations because they are distinctive.
A named port connection is a dot, an identifier, and an open parenthesis, at the start of a line:
$ /usr/bin/grep -rnE '^[[:space:]]*\.[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*\(' --include='*.sv' tb
tb/agents/axi_mon.sv:4: .clk (clk),
tb/agents/axi_mon.sv:5: .reset_n (reset_n),
tb/agents/axi_mon.sv:6: .awvalid (awvalid),
tb/agents/axi_mon.sv:7: .awready (),
tb/agents/axi_mon.sv:8: .awaddr ('0)Five connections, including .awready () — an unconnected output, which is exactly the kind of thing you search for deliberately:
# outputs left unconnected
/usr/bin/grep -rnE '^[[:space:]]*\.[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*\([[:space:]]*\)' --include='*.sv' .That pattern is worth keeping. An accidentally unconnected port is a defect no compiler is obliged to report, and it reads as a deliberate choice in the source.
A parameter override is #( followed by a dot:
$ /usr/bin/grep -rnE '#\([[:space:]]*\.[A-Za-z_]' --include='*.sv' .
./tb/agents/axi_mon.sv:3: axi_slave #(.ADDR_W(16), .DATA_W(32)) u_slv (One, and the question behind it is usually "where is this parameter overridden" rather than "where are overrides" — so the useful form names the parameter:
/usr/bin/grep -rnE '\.ADDR_W[[:space:]]*\(' --include='*.sv' .which finds both the override and any port connection of the same name. That ambiguity is real: .ADDR_W(16) inside #( ) is a parameter and inside a port list is a port, and the two are indistinguishable on their own line. The disambiguator is whether the line also contains #(, or the preceding line does.
5. Cross-Checking a Pattern
§2's defect — two hits where three were right — is only visible if you know the answer. Three ways to know it, and all three are cheap.
Count the same thing a different way. Instantiations and hierarchy scopes are the same set, and Chapter 23.4 §4 showed the tool will tell you:
$ vvp -v work/sim.vvp < /dev/null 2>&1 | grep -i scope
... 5 scopesFive scopes for a design with a top, a DUT and two leaves plus the testbench — so a pattern reporting two instantiations under chip_top is consistent and one reporting one is not.
Run the pattern on a fixture whose answer you wrote. Three instantiations in one file, one of each form, and assert that the pattern finds three. That is Chapter 21.8 applied to a regex, and it is how §2's defect was found:
#!/usr/bin/env bash
# pattern-check — assert an instantiation pattern finds all the forms
set -o nounset
GREP=${GREP:-/usr/bin/grep}
P=${1:?usage: pattern-check <ere>}
f=$(mktemp); trap 'rm -f -- "$f"' EXIT
cat > "$f" <<'V'
axi_slave u_plain (.clk(clk));
axi_slave #(.W(8)) u_param (.clk(clk));
axi_slave #(.W(8), .D(4)) u_two (.clk(clk));
axi_slave slv0 (.clk(clk));
V
n=$("$GREP" -cE "$P" "$f")
printf 'pattern matched %s of 4 forms\n' "$n"
"$GREP" -vE "$P" "$f" | sed 's/^/ MISSED:/'
[ "$n" -eq 4 ]And read the misses, not the hits. grep -v with the same pattern prints what it did not match, which is where a defect lives. A pattern you have only ever seen succeed is the regex version of a test suite you have only ever seen pass.
6. The Pattern Library
The four shapes, as a sourceable file — Chapter 18.6 is where this belongs:
# hdl-patterns.sh — source this, then use the functions
# Every pattern is POSIX ERE so it runs under BSD and GNU grep alike.
GREP=${GREP:-/usr/bin/grep}
HDL=(--include='*.sv' --include='*.svh' --include='*.v' --include='*.vh')
ID='[a-zA-Z_][a-zA-Z0-9_]*'
KW='module|endmodule|function|task|interface|package|class|program|primitive'
KW="$KW|if|else|case|casex|casez|for|while|repeat|forever|always|always_ff"
KW="$KW|always_comb|always_latch|initial|final|assign|return|generate|endgenerate"
NOKW=":[0-9]+:[[:space:]]*($KW)[^a-zA-Z0-9_]"
hdl_module() { "$GREP" -rnE "${HDL[@]}" "^[[:space:]]*module[[:space:]]+${1:-$ID}[^a-zA-Z0-9_]" "${@:2}"; }
hdl_endmodule(){ "$GREP" -rnE "${HDL[@]}" '^[[:space:]]*endmodule' "$@"; }
hdl_inst() { "$GREP" -rnE "${HDL[@]}" "^[[:space:]]*${1:-$ID}([[:space:]]*#\(.*\))?[[:space:]]+$ID[[:space:]]*\(" "${@:2}" |
"$GREP" -vE "$NOKW"; }
hdl_port() { "$GREP" -rnE "${HDL[@]}" "^[[:space:]]*\.${1:-$ID}[[:space:]]*\(" "${@:2}"; }
hdl_unconn() { "$GREP" -rnE "${HDL[@]}" "^[[:space:]]*\.$ID[[:space:]]*\([[:space:]]*\)" "$@"; }
hdl_param() { "$GREP" -rnE "${HDL[@]}" "#\([[:space:]]*\.[A-Za-z_]" "$@"; }
hdl_decl() { "$GREP" -rnE "${HDL[@]}" "(input|output|inout)[^;]*[^a-zA-Z0-9_]${1:?signal}[^a-zA-Z0-9_]" "${@:2}"; }$ . ./hdl-patterns.sh
$ hdl_inst axi_slave tb
tb/agents/axi_mon.sv:3: axi_slave #(.ADDR_W(16), .DATA_W(32)) u_slv (
$ hdl_unconn .
./tb/agents/axi_mon.sv:7: .awready (),
$ hdl_decl awvalid rtl
rtl/axi/axi_slave.sv:4: input logic awvalid, output logic awready,Five decisions worth naming:
${1:-$ID} for the name, so hdl_module with no argument lists every module and with an argument finds one. One function, two questions.
"${@:2}" for the directories, so argument one is the name and everything after is scope. That is a positional convention worth being consistent about — Chapter 15.4 §5's "take one, pass the rest".
${1:?signal} in hdl_decl, because a declaration search with no signal name is meaningless rather than general. Chapter 20.3.
HDL and the keyword list built once at source time rather than per call. The KW string is assembled in three steps purely for line length — a long single line is harder to extend correctly.
And GREP pinned, for Chapter 23.5 §3's measured reason: the library must behave the same in a script as at a prompt.
Practice
- Run the module-header pattern on your repository. Does the count match
git grep -c '^module'? - Find a module header the pattern misses. (Try one inside a
`ifdef, or with a comment betweenmoduleand the name.) - Build a file with four instantiation forms — plain, one parameter, two parameters, non-
u_name — and run both §2 patterns on it. Record which finds how many. - Explain, in one sentence, why
[^)]*fails on#(.ADDR_W(16)). - Replace
[^)]*with.*and find a line it over-matches. - Run the convention-free pattern and count how many hits are module headers.
- Add the keyword filter. Then remove the
:[0-9]+:anchor and confirm the filter stops working. - Write the multi-line instantiation from §3's callout and confirm no single-line pattern connects its type to its name.
- Run the unconnected-port pattern across your whole tree. Is every hit deliberate?
- Write
pattern-checkand use it on a pattern of your own. Then break the pattern and confirm it reports the miss. - Source
hdl-patterns.shand answer three real questions with it. - Add a seventh function for something you search for often, and give it a fixture.
Check Your Understanding
- Why does searching for a construct remove comments for free?
- What four parts of the module-header pattern are load-bearing?
- Why must these patterns be written with
-E? - Why does
#\([^)]*\)miss a parameterised instantiation? - What is the general rule about negated character classes and nesting?
- Why is a too-low count the dangerous failure?
- What do five of the eleven convention-free hits turn out to be?
- Why does the keyword filter need a second
grep? - What can no single-line pattern do, and what are the three honest responses?
- Why read a pattern's misses rather than its hits?
7. Remember
- Match the shape, not the string. A comment does not have the shape of a declaration.
- Four shapes cover most searches: module header, instantiation, port connection, parameter override.
- Anchor with
^[[:space:]]*and spell identifiers as[a-zA-Z_][a-zA-Z0-9_]*to stay POSIX. #\([^)]*\)misses every parameterised instantiation — measured: 2 hits instead of 3. Use#\(.*\).- A negated class cannot span nested delimiters, and regex cannot match balanced parens at all.
- A too-low count is the dangerous failure: it looks like a smaller design.
- The
u_convention costs youdutandslv0. Dropping it gives 11 hits of which 5 are module headers. - Filter keywords with a second
grep— BSD has no-P— and anchor the filter with:[0-9]+:. - A multi-line parameter list puts the type and the name on different lines. No single-line pattern can join them.
.port ()finds unconnected ports, which no compiler must report.- Cross-check a pattern against a fixture whose answer you wrote, and against the tool's own scope count.
- Read the misses.
grep -vwith the same pattern is where the defect is. - Keep the patterns in a sourceable library, with the grep pinned.
8. What Is Next
Chapter 24.3 takes these patterns to a tree that is not only SystemVerilog — Verilog, C and Python in one repository — and measures the --include form from this module's own curriculum entry, which finds nothing at all.
Where this fits
Part of the Linux & Bash curriculum.
