Perl Part II · Module 20 · Netlist Processing
Extracting Module and Instance Declarations
Having found an instantiation, the useful work begins: pulling out the cell type, the instance name and the individual port connections, so that every later question — what kind of cell is this, what is it wired to, where does it sit in the hierarchy — becomes a lookup rather than another pass over the text. The extraction is three capture groups and looks finished in a line. This chapter measures the one place that line is wrong, and it is a comma that is not a separator.
Advanced14 min readPerlPart IINetlists
Part II · Module 20 · Chapter 20.3 · Extracting Module and Instance Declarations
1. The Three Parts
Every instantiation is a cell type, an instance name and a connection list, and one pattern gets all three out of the accumulated statement:
next unless $stmt =~ /^(\w+)\s+(\w+)\s*\((.*)\)$/;Type, name, and everything between the outer parentheses. Chapter 18.5's extraction, applied to a netlist statement instead of a log line — and Chapter 11.11's named captures are worth using here once there are three of them, because $1, $2, $3 stop being readable the moment somebody adds a fourth.
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, now KEEPING the connection text
sub instances {
my ($file) = @_;
open my $fh, "<", $file or die "$file: $!";
my (@inst, $cur, $buf, $in_block);
while (my $raw = <$fh>) {
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, { type => $1, name => $2, parent => $cur, conns => $3 };
}
close $fh;
return @inst;
}
# ---- A: split the connection list on every comma
sub ports_naive {
my ($conns) = @_;
my @p;
for my $frag (split /,/, $conns) {
push @p, [$1, $2] if $frag =~ /\.(\w+)\s*\(\s*(.*?)\s*\)/;
}
return @p;
}
# ---- B: split only on commas at depth zero
sub ports_depth {
my ($conns) = @_;
# NOTE: `my (@frag, $cur, $depth) = ((), "", 0);` does NOT work --
# an array on the left of a list assignment slurps the whole list,
# leaving $cur and $depth undef. Declare them separately.
my @frag;
my $cur = "";
my $depth = 0;
for my $ch (split //, $conns) {
if ($ch =~ /[({\[]/) { $depth++ }
elsif ($ch =~ /[)}\]]/) { $depth-- }
if ($ch eq "," && $depth == 0) { push @frag, $cur; $cur = ""; next }
$cur .= $ch;
}
push @frag, $cur if $cur =~ /\S/;
my @p;
for my $f (@frag) {
push @p, [$1, defined $2 ? $2 : ""] if $f =~ /\.(\w+)\s*\(\s*(.*?)\s*\)\s*$/s;
}
return @p;
}
my @CONN_CASES = (
["plain named ports", ".A1(a), .A2(b), .Z(y)", 3],
["a bus index in a net", ".A(count[0]), .ZN(n11)", 2],
["an EMPTY connection", ".A(rst_n), .Z()", 2],
["a CONCATENATION in a port", ".A1({a,b}), .A2(a), .Z(y)", 3],
["POSITIONAL connections", "a, n1", 0],
);
printf "%-32s %-7s %-9s %-9s %s\n", "CONNECTION LIST", "PORTS", "A split", "B depth", "verdict";
for my $c (@CONN_CASES) {
my ($label, $conns, $truth) = @$c;
my @a = ports_naive($conns);
my @b = ports_depth($conns);
printf "%-32s %-7d %-9d %-9d %s\n", $label, $truth, scalar @a, scalar @b,
(@a == $truth && @b == $truth ? "both ok"
: @b == $truth ? "only B correct" : "BOTH WRONG");
}
print "\nthe concatenation case, in detail:\n";
printf " A sees: %s\n", join(" ", map { ".$_->[0]($_->[1])" } ports_naive(".A1({a,b}), .A2(a), .Z(y)"));
printf " B sees: %s\n", join(" ", map { ".$_->[0]($_->[1])" } ports_depth(".A1({a,b}), .A2(a), .Z(y)"));
print "\nPOSITIONAL connections give ZERO named ports -- which looks exactly\n";
print "like an instance with nothing connected. Detect them:\n";
for my $c (@CONN_CASES) {
my ($label, $conns) = @$c;
my $named = ($conns =~ /\.\w+\s*\(/) ? 1 : 0;
printf " %-32s named-port form? %s\n", $label, ($named ? "yes" : "NO -- positional");
}
# ---- the real netlist
print "\n", "-" x 66, "\n";
my @inst = instances("netlist/counter_syn.v");
printf "counter_syn.v: %d instantiations\n\n", scalar @inst;
printf "%-14s %-12s %-14s %s\n", "INSTANCE", "TYPE", "PARENT", "PORTS";
for my $i (@inst) {
my @p = ports_depth($i->{conns});
printf "%-14s %-12s %-14s %s\n", $i->{name}, $i->{type}, $i->{parent},
(@p ? join(",", map { $_->[0] } @p) : "(none parsed)");
}
# ---- selection by pattern
print "\nselect by type or name pattern:\n";
my @flops = grep { $_->{type} =~ /^DF/ } @inst;
my @clk = grep { $_->{type} =~ /^CK/ } @inst;
my @subs = grep { $_->{type} =~ /^counter_/ } @inst;
my @u = grep { $_->{name} =~ /^U\d+$/ } @inst;
printf " flops (type ^DF) %d: %s\n", scalar @flops, join(" ", map { $_->{name} } @flops);
printf " clock (type ^CK) %d: %s\n", scalar @clk, join(" ", map { $_->{name} } @clk);
printf " submods (type ^counter_) %d: %s\n", scalar @subs, join(" ", map { $_->{name} } @subs);
printf " name ^U<digits> %d\n", scalar @u;CONNECTION LIST PORTS A split B depth verdict
plain named ports 3 3 3 both ok
a bus index in a net 2 2 2 both ok
an EMPTY connection 2 2 2 both ok
a CONCATENATION in a port 3 2 3 only B correct
POSITIONAL connections 0 0 0 both ok
the concatenation case, in detail:
A sees: .A2(a) .Z(y)
B sees: .A1({a,b}) .A2(a) .Z(y)
POSITIONAL connections give ZERO named ports -- which looks exactly
like an instance with nothing connected. Detect them:
plain named ports named-port form? yes
a bus index in a net named-port form? yes
an EMPTY connection named-port form? yes
a CONCATENATION in a port named-port form? yes
POSITIONAL connections named-port form? NO -- positional
------------------------------------------------------------------
counter_syn.v: 18 instantiations
INSTANCE TYPE PARENT PORTS
U1 INVD1 counter_bit A,ZN
U2 INVD1 counter_bit A,ZN
state_reg DFCNQD1 counter_bit D,CP,CDN,Q
clk_gate CKLNQD1 counter_top CP,E,TE,ECK
clk_buf CKBD1 counter_top I,Z
U20 INVD1 counter_top A,ZN
bit0 counter_bit counter_top D,CP,CDN,Q
U21 XOR2D1 counter_top A1,A2,Z
bit1 counter_bit counter_top D,CP,CDN,Q
U22 AN2D1 counter_top A1,A2,Z
U23 XOR2D1 counter_top A1,A2,Z
bit2 counter_bit counter_top D,CP,CDN,Q
U24 AN2D1 counter_top A1,A2,Z
U25 XOR2D1 counter_top A1,A2,Z
bit3 counter_bit counter_top D,CP,CDN,Q
U26 BUFFD1 counter_top A,Z
U27 TIELO counter_top ZN
FILLER_1 FILL1 counter_top (none parsed)
select by type or name pattern:
flops (type ^DF) 1: state_reg
clock (type ^CK) 2: clk_gate clk_buf
submods (type ^counter_) 4: bit0 bit1 bit2 bit3
name ^U<digits> 10The table of all eighteen instances is what the rest of the module works from: instance name, cell type, enclosing module, and the port names found in the connection list. FILL1 FILLER_1 shows an empty connection list correctly reported as none — a filler cell has no ports.
2. A Comma Is Not Always A Separator
The connection list looks like a comma-separated list, so splitting it on commas is the obvious move. The measurement shows where that stops working:
| connection list | ports | naive split | depth-aware |
|---|---|---|---|
| plain named ports | 3 | 3 | 3 |
| a bus index in a net | 2 | 2 | 2 |
| an empty connection | 2 | 2 | 2 |
| a concatenation in a port | 3 | 2 | 3 |
| positional connections | 0 | 0 | 0 |
.A1({a,b}) contains a comma that is not a port separator. Split on it and the first fragment becomes .A1({a, which matches nothing — so the port is not merely mis-parsed, it is dropped. The detail line in the output shows it: the naive version sees .A2(a) .Z(y) and has silently lost .A1 altogether.
The fix is to split only on commas at depth zero:
for my $ch (split //, $conns) {
if ($ch =~ /[({\[]/) { $depth++ }
elsif ($ch =~ /[)}\]]/) { $depth-- }
if ($ch eq "," && $depth == 0) { push @frag, $cur; $cur = ""; next }
$cur .= $ch;
}Counting all three bracket kinds matters: {} for concatenations, () for the port wrapper and any expression, [] for bus indices and part-selects. A netlist connection like .D({n1, n2[3:0]}) uses all three at once.
3. Positional Connections Look Like Nothing Connected
The last row is the one to be careful about. INVD1 U2 ( a, z ); is legal Verilog — connections by position instead of by name — and it was verified by compiling it.
Both parsers report zero named ports. That is arithmetically correct and practically dangerous, because an instance with zero parsed ports is indistinguishable from:
- a cell that genuinely has no ports, like
FILL1 - an instance whose connection list the parser failed on
- an instance with all its pins left unconnected
Three quite different situations, one empty list. So detect the form rather than reporting the count:
my $named = ($conns =~ /\.\w+\s*\(/) ? 1 : 0;If the list has no .port( shape at all and is non-empty, it is positional, and the honest report says so:
warn "instance $name uses POSITIONAL connections; "
. "port names are not available from the text\n";Positional connections cannot be turned into port names without the cell's own definition — the information is not in the netlist. That is Chapter 18.8's rule again: a parser that meets something it cannot resolve says so rather than returning a plausible empty answer.
Modern synthesis tools emit named connections, so this is mostly a hazard in older netlists and hand-edited ones. It costs one regex to rule out.
4. The Gotcha That Only The Warning Showed
Worth recording because it produced correct output while being wrong:
my (@frag, $cur, $depth) = ((), "", 0); # does NOT do what it reads likeAn array on the left of a list assignment slurps the whole list. @frag takes everything, and $cur and $depth are left undef. The code then ran, $depth++ turned undef into 1, the comparisons worked out, and the port counts came out right.
The only sign was one line on standard error:
Use of uninitialized value $depth in numeric eq (==)Which is Chapter 14.2's point exactly — the warning goes to stderr, and a script whose stdout looks correct is a script nobody looks at stderr for. The fix is three separate declarations, and the habit is to read stderr even when stdout is right.
5. Selecting By Type Or Name
Once every instance is a hash with type, name, parent and conns, selection is grep (Chapter 5.10) and the patterns are the library's naming convention:
my @flops = grep { $_->{type} =~ /^DF/ } @inst; # measured: 1
my @clk = grep { $_->{type} =~ /^CK/ } @inst; # measured: 2
my @subs = grep { $_->{type} =~ /^counter_/ } @inst; # measured: 4The measured counts are in the output. Two notes on doing this reliably:
Anchor the type patterns. /^DF/ and not /DF/, or a cell named MUXDFX1 joins the flop list. Chapter 11.8's anchors, and the same lesson Chapter 19.2 reached about filename filters.
A hierarchical instance is not a cell. counter_bit bit0 has a "type" that is a module in the same file, and the four of them are not gates. Separating them is what makes Chapter 20.4's hierarchy map possible, and the test is whether the type appears as a module name in this netlist — which the scan already collected.
6. What To Remember
- An instantiation is three parts: cell type, instance name, connection list — one pattern gets all three.
- Use named captures once there are three groups (11.11);
$3stops being readable fast. - Keep the enclosing module with each instance — it is what 20.4 needs.
- Measured: splitting the connection list on every comma reports 2 ports for a 3-port cell.
.A1({a,b})contains a comma that is not a separator, and the fragment.A1({amatches nothing, so the port is dropped rather than mis-parsed.- Split only on commas at depth zero, counting
(),{}and[]— a real connection can use all three. - The depth counter is a tiny parser, and it is safe here only because connection lists are bracket-only: no comments, macros or strings (20.2).
- Measured: positional connections give zero named ports — indistinguishable from a port-less cell, a parse failure, or everything unconnected.
- Detect the named-port form with
/\.\w+\s*\(/and report positional connections explicitly; port names are not in the text (18.8). - Both the concatenation and the positional form were verified legal by compiling them.
my (@frag, $cur, $depth) = ((), "", 0);does not work — an array on the left slurps the whole list, leaving the scalarsundef.- That bug produced correct output. The only sign was a warning on stderr (14.2) — read stderr even when stdout looks right.
- Select with
grepon anchored type patterns —/^DF/, not/DF/, orMUXDFX1becomes a flop. - A hierarchical instance is not a cell: test whether its type is a module declared in this netlist.
- Keep cell-family patterns as data, not logic — the library varies, the parser should not.
Chapter 20.4 uses the parent information every instance now carries. A parent-to-children map turns the flat list into the design's hierarchy, which answers where a block sits and how many instances it contains without opening a tool.
Where this fits
Part of the Perl curriculum.
