Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
extract.pl
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;
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
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>           10

The 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 listportsnaive splitdepth-aware
plain named ports333
a bus index in a net222
an empty connection222
a concatenation in a port323
positional connections000

.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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
my (@frag, $cur, $depth) = ((), "", 0);     # does NOT do what it reads like

An 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
my @flops = grep { $_->{type} =~ /^DF/ }        @inst;   # measured: 1
my @clk   = grep { $_->{type} =~ /^CK/ }        @inst;   # measured: 2
my @subs  = grep { $_->{type} =~ /^counter_/ }  @inst;   # measured: 4

The 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); $3 stops 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({a matches 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 scalars undef.
  • That bug produced correct output. The only sign was a warning on stderr (14.2) — read stderr even when stdout looks right.
  • Select with grep on anchored type patterns — /^DF/, not /DF/, or MUXDFX1 becomes 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.