Perl Part II · Module 20 · Netlist Processing
What a Regex Can and Cannot See in a Netlist
The previous chapter's scan works, and it is worth knowing exactly how far it works. This chapter takes eight constructs that all appear in real synthesised output, puts each one in its own tiny netlist so that no two hazards can be confused, and counts what the scan finds. Half of them defeat it — and the important part is the shape of the failure: the scan does not report an error or a wrong cell type, it simply returns a shorter list, which every analysis downstream then treats as the truth.
Advanced16 min readPerlPart IINetlists
Part II · Module 20 · Chapter 20.2 · What a Regex Can and Cannot See in a Netlist
1. Eight Hazards, Each On Its Own
Every case below is wrapped in its own one-instance module, so a failure can be attributed to exactly one cause.
use strict;
use warnings;
my @KEYWORDS = qw(module endmodule input output inout wire reg assign
parameter localparam always initial generate endgenerate
genvar begin end supply0 supply1);
my %IS_KW = map { $_ => 1 } @KEYWORDS;
# the 20.1 scan, operating on a string so each hazard can be isolated
sub scan_text {
my ($text) = @_;
my (@inst, $cur, $buf, $in_block);
for my $raw (split /^/, $text) {
my $l = $raw;
if ($in_block) { if ($l =~ s{.*?\*/}{}) { $in_block = 0 } else { next } }
$l =~ s{/\*.*?\*/}{}g;
if ($l =~ s{/\*.*$}{}) { $in_block = 1 }
$l =~ s{//.*$}{};
next unless $l =~ /\S/;
if ($l =~ /^\s*module\s+(\w+)/) { $cur = $1; $buf = ""; next }
if ($l =~ /^\s*endmodule\b/) { undef $cur; $buf = ""; next }
next unless defined $cur;
$buf .= " $l";
next unless $buf =~ /;/;
my $stmt = $buf; $buf = "";
$stmt =~ s/;.*$//; $stmt =~ s/\s+/ /g; $stmt =~ s/^ | $//g;
next unless $stmt =~ /^(\w+)\s+(\w+)\s*\((.*)\)$/;
next if $IS_KW{$1};
push @inst, "$1 $2";
}
return @inst;
}
sub wrap { my ($body) = @_; return "module m (a);\n input a;\n wire n;\n$body\nendmodule\n" }
# Each case holds exactly ONE instantiation in its text, plus one hazard.
my @CASES = (
["plain (the control)", " INVD1 U1 ( .A(a), .ZN(n) );\n", 1],
["escaped identifier", " INVD1 \\U1/esc ( .A(a), .ZN(n) );\n", 1],
["type and name split by /* */"," INVD1 /* ECO */ U1 ( .A(a), .ZN(n) );\n", 1],
["cell type from a macro", " BUFF`DRIVE U1 ( .A(a), .Z(n) );\n", 1],
["two instances on one line", " INVD1 U1 ( .A(a), .ZN(n) ); INVD1 U2 ( .A(n), .ZN() );\n", 2],
["inside a generate block", " genvar i;\n generate for (i=0;i<2;i=i+1) begin : g\n"
. " INVD1 U1 ( .A(a), .ZN() );\n end endgenerate\n", 1],
["after a generate block", " genvar i;\n generate for (i=0;i<2;i=i+1) begin : g\n"
. " INVD1 U1 ( .A(a), .ZN() );\n end\n endgenerate\n"
. " INVD1 U2 ( .A(a), .ZN(n) );\n", 2],
["concatenation in a port", " AN2D1 U1 ( .A1(a), .A2(a), .Z(n) );\n"
. " BUFFD1 U2 ( .A({a}), .Z() );\n", 2],
);
printf "%-32s %-7s %-7s %s\n", "HAZARD", "IN TEXT", "FOUND", "verdict";
my ($ok, $bad) = (0, 0);
for my $c (@CASES) {
my ($label, $body, $truth) = @$c;
my @got = scan_text(wrap($body));
my $good = (@got == $truth);
$good ? $ok++ : $bad++;
printf "%-32s %-7d %-7d %s\n", $label, $truth, scalar @got,
($good ? "read correctly" : "SCAN IS WRONG");
}
printf "\n%d of %d hazards read correctly; %d defeat the scan\n",
$ok, scalar @CASES, $bad;
print "\nhow each failure fails, measured one at a time:\n";
my %detail = (
"escaped identifier" => " INVD1 \\U1/esc ( .A(a), .ZN(n) );\n",
"macro cell type" => " BUFF`DRIVE U1 ( .A(a), .Z(n) );\n",
"two on one line" => " INVD1 U1 ( .A(a), .ZN(n) ); INVD1 U2 ( .A(n), .ZN() );\n",
);
for my $k (sort keys %detail) {
my @got = scan_text(wrap($detail{$k}));
printf " %-20s -> %s\n", $k, (@got ? join(" | ", @got) : "NOTHING found");
}
print "\nso three of the four failures are SILENT OMISSIONS, not wrong values:\n";
print "the instance simply is not in the result, and no count says so.\n";HAZARD IN TEXT FOUND verdict
plain (the control) 1 1 read correctly
escaped identifier 1 0 SCAN IS WRONG
type and name split by /* */ 1 1 read correctly
cell type from a macro 1 0 SCAN IS WRONG
two instances on one line 2 1 SCAN IS WRONG
inside a generate block 1 1 read correctly
after a generate block 2 1 SCAN IS WRONG
concatenation in a port 2 2 read correctly
4 of 8 hazards read correctly; 4 defeat the scan
how each failure fails, measured one at a time:
escaped identifier -> NOTHING found
macro cell type -> NOTHING found
two on one line -> INVD1 U1
so three of the four failures are SILENT OMISSIONS, not wrong values:
the instance simply is not in the result, and no count says so.Four of eight read correctly; four defeat the scan. The control case — a plain instantiation — reads correctly, which is what makes the other rows meaningful.
Two results are worth stating plainly because they contradict what I expected:
A type and name split by a block comment reads correctly. INVD1 /* ECO */ U1 ( ... ); is on the registry's list of hard cases and on mine, and the measurement says it is not hard for this scan: comments are stripped before matching, so by the time the pattern runs the comment is gone. A defence already in place for one reason turned out to cover another.
A concatenation in a port connection reads correctly too. .A({a}) does not disturb the instance-level match, because the scan matches the statement's outer shape and never splits the connection list here. It becomes a problem only in Chapter 20.6, where the connections themselves are parsed.
2. The Three Silent Omissions
The four failures are not equally shaped, and the output's second block says how each one fails:
An escaped identifier finds nothing. \U1/esc is a legal Verilog name — a backslash, then any characters, terminated by whitespace. Synthesised netlists are full of them, because that is how a tool spells a name containing [, ], / or . after flattening hierarchy. (\w+) cannot match it, so the whole statement fails the pattern and the instance vanishes.
A macro in the cell type finds nothing. BUFFDRIVE U1 ( ... );— the type is assembled by the preprocessor, and a backtick is not a word character. I expected this to report a truncated type likeBUFF`; measured, it reports nothing at all.
Two instances on one line finds the first only. The statement buffer is truncated at the first ;, so the second instantiation on that line is discarded with the remainder.
And a statement after a generate block finds one of two. This one is a cascade rather than a direct failure: end and endgenerate carry no ;, so they accumulate into the statement buffer and corrupt the next statement, which then fails the shape match.
Three of the four failures are silent omissions. Not an error, not a bogus value — the instance is absent from the result and nothing in the output says a line was skipped. So a cell census is short, a hierarchy map has a missing child, and a QC check passes, all with numbers that look entirely reasonable.
3. The Boundary
Figure 20.2.1 — What each netlist construct needs to be read
The rows escalate in a specific way. Rows one and two are text problems and a text tool solves them. Row three needs a lexer — something that knows Verilog's rule that a name beginning with \ runs to the next whitespace. Row four needs the preprocessor, which means the macro definitions and therefore the include path. Row five needs elaboration, and that is categorically different: there is no amount of reading the file that tells you how many instances a generate loop produces when its bound is a parameter overridden somewhere else.
That is the same ladder Chapter 18.11 built for source code, arriving at the same place: at the bottom row the answer is not a better script, it is the tool's own output.
4. Text Count Versus Elaborated Count
The combined netlist makes this concrete. Its text holds 8 instantiations; the elaborated design has 9, because the generate loop produces two copies of one text instance. The scan found 4 — so it is short against both numbers, and the two numbers are not the same question.
This distinction matters more than it looks:
"How many cells are in this netlist file?" is a text question, and a scan can answer it if nothing on this page defeats it.
"How many cells are in this design?" is an elaboration question. Only the tool knows, and the answer can differ from the text count by orders of magnitude in a netlist that uses generate for a memory array.
So when a cell count has to be right — a signoff gate count, an area estimate, an instance-count delta between two runs — it comes from the tool's report, and Chapter 20.5's census is for the many cases where an approximate, fast, no-licence answer is what you need. Say which one you are producing.
5. What To Do Instead
Three options, in the order worth trying.
Make the scan report its own blind spots. You cannot parse the hard constructs, and you can detect that they are present:
$warn{escaped}++ if /\\\S+\s/;
$warn{macro}++ if /^\s*\w*`\w+/;
$warn{generate}++ if /^\s*generate\b/;
...
print STDERR "warning: netlist contains $warn{generate} generate block(s);\n"
. " the instance count below is a LOWER BOUND\n" if $warn{generate};That is Chapter 18.8's rule applied here — a parser that meets something it does not recognise must say so rather than return a number. It costs three regexes and turns a silent omission into a stated limitation.
Get the tool to flatten it. A simulator or synthesis tool asked to write out an elaborated netlist produces exactly the regular text the scan wants, with generate blocks expanded and macros resolved. One tool invocation removes rows four and five entirely.
Use the tool's report for the number. A hierarchy report or a cell census from the tool is authoritative and is itself regular text — which puts you back in Module 18's territory, reading a report rather than guessing at source. Chapter 18.11 ended with the same recommendation and it is worth repeating because it is the one people skip: the scan is for when you cannot run the tool, not for when running it is inconvenient.
6. What To Remember
- The scan from 20.1 works, and this chapter is how far.
- Measured, each hazard isolated: 4 of 8 read correctly, 4 defeat the scan, with a control case that must pass.
- Isolate one hazard per fixture. My combined fixture gave the right count with the wrong attribution — it blamed a shared line when the cause was
endgenerateleaking into the buffer. - A type/name split by a block comment reads correctly — contrary to my expectation; comment stripping already covers it.
- A concatenation in a port reads correctly at instance level; it matters only in 20.6.
- An escaped identifier finds NOTHING.
\U1/escis legal, ends at whitespace, and(\w+)cannot match it — and flattened netlists are full of them. - A macro in the cell type finds NOTHING — not a truncated type, as I expected; nothing.
- Two instances on one line finds the first only — the buffer truncates at the first
;. - A statement after
endgenerateis corrupted, becauseend/endgeneratecarry no;and accumulate. - Three of the four failures are silent omissions — no error, no wrong value, just a shorter list that everything downstream believes.
- Every construct here is legal: the compiler elaborates the netlist and it simulates (
q=11,PASSED). - The ladder: line scan → strip and scan → a lexer → the preprocessor → elaboration.
- Text count and elaborated count are different questions — 8 in the text, 9 after elaboration, 4 found.
- A flattened post-layout netlist has none of the hard constructs, which is why this Perl job survived; the danger is applying it to a netlist that kept hierarchy.
- Grep for
generate, a backtick, and a backslash-space before trusting a scan's count. - Make the scan report its own blind spots (18.8) — three regexes turn a silent omission into a stated lower bound.
- Prefer a tool-flattened netlist, or the tool's own report, when the number has to be right.
Chapter 20.3 goes back inside the instance. Having found an instantiation, the next job is pulling the cell type, the instance name and the individual port connections out of it — including the multi-line form, and including a connection list that does not split on commas as simply as it looks.
Where this fits
Part of the Perl curriculum.
