Skip to content
VLSI Mentor

Perl Part II · Module 20 · Netlist Processing

Reading a Gate-Level Netlist Line by Line

Reading netlists is one of the oldest Perl jobs in the industry, and it survives because a flat synthesised netlist is extremely regular text: one instantiation after another, each naming a cell type, an instance name and a connection list. That regularity is real and it is also slightly less complete than it looks. This chapter builds the scan that every later chapter in the module depends on, and measures it against a netlist that is not a sample of plausible text but a working gate-level design that compiles and simulates.

Intermediate15 min readPerlPart IINetlists

Part II · Module 20 · Chapter 20.1 · Reading a Gate-Level Netlist Line by Line

1. What A Synthesised Netlist Looks Like

A gate-level netlist is the design after synthesis: no always_comb, no +, no parameters — just cells from a library, wired together. The fixture used throughout this module is a four-bit counter, and its shape is the shape of every netlist you will meet.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module counter_top (
    clk,
    rst_n,
    enable,
    count );
  input clk;
  input rst_n;
  output [3:0] count;
  wire gclk, n10, n11, n12;

  CKLNQD1 clk_gate ( .CP(clk), .E(enable), .TE(1'b0), .ECK(gclk) );
  INVD1   U20 ( .A(count[0]), .ZN(n11) );
  counter_bit bit0 ( .D(n11), .CP(n10), .CDN(rst_n), .Q(count[0]) );
endmodule

Five things to notice, because the scan depends on all of them:

The port list is separate from the declarations. The module header names the ports; input/output lines declare their directions afterwards. That is the old-style Verilog form, and synthesis tools still emit it.

Every instantiation has the same three parts: a cell type, an instance name, and a connection list of .port(net) pairs.

Net names are generated — n10, n11, gclk. They carry no meaning and they change on every resynthesis, which matters for Chapter 20.7's comparisons.

Instances can be hierarchical. counter_bit bit0 instantiates another module in the same file, not a library cell. Nothing in the syntax distinguishes the two; only knowing the library does.

Cell names encode function and drive strength — INVD1 is an inverter at drive 1, ND2D1 a two-input NAND. Chapter 20.5 classifies on exactly this.

2. Four Scans, One Netlist

The netlist declares 2 modules and holds 18 instantiations — 3 inside counter_bit, 15 inside counter_top. Those numbers are the truth the scans are measured against.

Azvya Education Pvt. Ltd.VLSI Mentor
readnl.pl
use strict;
use warnings;

my $NL = "netlist/counter_syn.v";

# The netlist declares 2 modules and holds 18 instantiations in total
# (3 inside counter_bit, 15 inside counter_top). Counted by hand from the
# file, and cross-checked against the compiler at the end of this chapter.
my $TRUTH_MODULES   = 2;
my $TRUTH_INSTANCES = 18;

sub slurp_lines { open my $fh, "<", $NL or die "$NL: $!"; my @l = <$fh>; close $fh; return @l }

# ---- A: the obvious single-line instance pattern
sub scan_naive {
    my ($mods, $insts) = (0, 0);
    for my $l (slurp_lines()) {
        $mods++  if $l =~ /^\s*module\s+(\w+)/;
        $insts++ if $l =~ /^\s*(\w+)\s+(\w+)\s*\(.*\)\s*;/;
    }
    return ($mods, $insts);
}

# ---- B: strip comments first, then the same pattern
sub strip_comments {
    my $t = join "", slurp_lines();
    $t =~ s{/\*.*?\*/}{}gs;
    $t =~ s{//[^\n]*}{}g;
    return split /^/, $t;
}
sub scan_stripped {
    my ($mods, $insts) = (0, 0);
    for my $l (strip_comments()) {
        $mods++  if $l =~ /^\s*module\s+(\w+)/;
        $insts++ if $l =~ /^\s*(\w+)\s+(\w+)\s*\(.*\)\s*;/;
    }
    return ($mods, $insts);
}

# ---- C: join each statement up to its ';' before matching (the real fix)
my @KEYWORDS = qw(module endmodule input output inout wire reg assign
                  parameter localparam always initial endgenerate generate);
my %IS_KW = map { $_ => 1 } @KEYWORDS;
sub statements {
    my $t = join "", slurp_lines();
    $t =~ s{/\*.*?\*/}{}gs;              # block comments
    $t =~ s{//[^\n]*}{}g;                # line comments
    my @stmt;
    for my $raw (split /;/, $t) {
        $raw =~ s/\s+/ /g;
        $raw =~ s/^ | $//g;
        push @stmt, $raw if $raw =~ /\S/;
    }
    return @stmt;
}
sub scan_statements {
    my ($mods, @inst) = (0);
    for my $s (statements()) {
        $mods++ if $s =~ /^module\s+(\w+)/;
        # an instantiation is: TYPE name ( ... )  and TYPE is not a keyword
        if ($s =~ /^(\w+)\s+(\w+)\s*\((.*)\)$/s) {
            my ($type, $name) = ($1, $2);
            next if $IS_KW{$type};
            push @inst, "$type $name";
        }
    }
    return ($mods, scalar @inst, \@inst);
}

my ($ma, $ia) = scan_naive();
my ($mb, $ib) = scan_stripped();
my ($mc, $ic, $list) = scan_statements();

printf "the netlist declares %d modules and %d instantiations\n\n",
    $TRUTH_MODULES, $TRUTH_INSTANCES;
printf "%-40s %-9s %-11s %s\n", "SCAN", "MODULES", "INSTANCES", "verdict";
for my $r (["A  single-line pattern",            $ma, $ia],
           ["B  comments stripped, same pattern", $mb, $ib],
           ["C  joined to each ';'",              $mc, $ic]) {
    my ($label, $m, $i) = @$r;
    printf "%-40s %-9s %-11s %s\n", $label,
        ($m == $TRUTH_MODULES ? "$m ok" : "$m BAD"),
        ($i == $TRUTH_INSTANCES ? "$i ok" : "$i BAD"),
        ($m == $TRUTH_MODULES && $i == $TRUTH_INSTANCES ? "correct" : "WRONG");
}

print "\nwhich instantiation do A and B miss?\n";
my %found_c = map { $_ => 1 } @$list;
my %found_a;
for my $l (slurp_lines()) {
    $found_a{"$1 $2"} = 1 if $l =~ /^\s*(\w+)\s+(\w+)\s*\(.*\)\s*;/ && !$IS_KW{$1};
}
for my $i (@$list) { print "  missed by the single-line pattern: $i\n" unless $found_a{$i} }

print "\nthe 18 instantiations, as scan C sees them:\n";
printf "  %s\n", $_ for @$list;

# ---- D: the hybrid. module/endmodule are reliably LINE-oriented in a
# netlist; an instantiation is a STATEMENT that may span lines. Do both.
sub scan_hybrid {
    my (@mods, @inst, $cur, $buf);
    my $in_block = 0;
    for my $raw (slurp_lines()) {
        my $l = $raw;
        # strip comments line by line, tracking a block comment across lines
        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+)/)  { push @mods, $1; $cur = $1; $buf = ""; next }
        if ($l =~ /^\s*endmodule\b/)     { undef $cur;      $buf = ""; next }
        next unless defined $cur;

        $buf .= " $l";
        next unless $buf =~ /;/;                 # statement not finished yet
        my $stmt = $buf; $buf = "";
        $stmt =~ s/;.*$//; $stmt =~ s/\s+/ /g; $stmt =~ s/^ | $//g;
        next unless $stmt =~ /^(\w+)\s+(\w+)\s*\((.*)\)$/;
        my ($type, $name, $conns) = ($1, $2, $3);
        next if $IS_KW{$type};
        push @inst, { type => $type, name => $name, parent => $cur, conns => $conns };
    }
    return (\@mods, \@inst);
}
my ($mods_d, $inst_d) = scan_hybrid();
printf "\n%-40s %-9s %-11s %s\n", "D  hybrid: lines for modules, statements for instances",
    (@$mods_d == $TRUTH_MODULES ? scalar(@$mods_d)." ok" : scalar(@$mods_d)." BAD"),
    (@$inst_d == $TRUTH_INSTANCES ? scalar(@$inst_d)." ok" : scalar(@$inst_d)." BAD"),
    (@$mods_d == $TRUTH_MODULES && @$inst_d == $TRUTH_INSTANCES ? "correct" : "WRONG");

print "\nand because D tracks the enclosing module, it can report per module:\n";
my %per;
$per{$_->{parent}}++ for @$inst_d;
printf "  %-14s %d instantiation(s)\n", $_, $per{$_} for sort keys %per;

print "\ncross-check against the compiler: does the netlist elaborate and run?\n";
my $log = `iverilog -o nl.vvp lib/cells.v netlist/counter_syn.v tb/tb_syn.v 2>&1`;
my $rc  = $? >> 8;
if ($rc == 0) {
    my $run = `vvp nl.vvp 2>&1`;
    my ($c) = $run =~ /count=(\d+)/;
    printf "  rc=0, and the gate-level design simulates: count=%s, %s\n",
        (defined $c ? $c : "?"), ($run =~ /PASSED/ ? "PASSED" : "no PASSED line");
} else {
    printf "  rc=%d  %s\n", $rc, ((grep { /\S/ } split /\n/, $log)[0] // "");
}
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
the netlist declares 2 modules and 18 instantiations

SCAN                                     MODULES   INSTANCES   verdict
A  single-line pattern                   2 ok      17 BAD      WRONG
B  comments stripped, same pattern       2 ok      17 BAD      WRONG
C  joined to each ';'                    0 BAD     18 ok       WRONG

which instantiation do A and B miss?
  missed by the single-line pattern: AN2D1 U22

the 18 instantiations, as scan C sees them:
  INVD1 U1
  INVD1 U2
  DFCNQD1 state_reg
  CKLNQD1 clk_gate
  CKBD1 clk_buf
  INVD1 U20
  counter_bit bit0
  XOR2D1 U21
  counter_bit bit1
  AN2D1 U22
  XOR2D1 U23
  counter_bit bit2
  AN2D1 U24
  XOR2D1 U25
  counter_bit bit3
  BUFFD1 U26
  TIELO U27
  FILL1 FILLER_1

D  hybrid: lines for modules, statements for instances 2 ok      18 ok       correct

and because D tracks the enclosing module, it can report per module:
  counter_bit    3 instantiation(s)
  counter_top    15 instantiation(s)

cross-check against the compiler: does the netlist elaborate and run?
  rc=0, and the gate-level design simulates: count=12, PASSED

Read the verdict column. Three of the four scans are wrong, and they are wrong about different things.

A, the obvious single-line pattern, finds 17 of 18. The one it misses is named in the output: AN2D1 U22, which the netlist writes across four lines. A tool emits that form whenever the connection list is long, so in a real netlist it is not one instance in eighteen — it is most of them.

B, with comments stripped first, still finds 17. Stripping comments was the right thing to do and it fixed nothing here, which is worth seeing: a defensive step that addresses a hazard you do not have changes no numbers.

C, joining each statement up to its ;, finds all 18 instances — and reports zero modules.

That last one I did not predict. It is the chapter's second finding and section 3 is about it.

3. Why The Hybrid Is The Right Shape

Scan C splits the file on ; and matches /^module\s+(\w+)/ against each piece. The module count comes out 0.

The reason is that a statement is not a line. After the split, the piece containing the first module header begins with the `timescale directive that precedes it in the file, so module is no longer at the start and the anchor fails. Chapter 11.8's anchors are doing exactly what they are told; the text they are anchored to has changed shape.

So the two questions want two different granularities:

module and endmodule are reliably line-oriented. A synthesised netlist puts them at the start of their own lines, always. Scanning lines answers this correctly, which is why A and B both got 2.

An instantiation is a statement. It ends at a ; that may be several lines away. Answering this needs accumulation — Chapter 18.6's multi-line record, with the ; as the record terminator instead of a blank line.

Scan D does both, and it is the scan the rest of the module uses:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if ($l =~ /^\s*module\s+(\w+)/)  { push @mods, $1; $cur = $1; $buf = ""; next }
if ($l =~ /^\s*endmodule\b/)     { undef $cur;      $buf = ""; next }
next unless defined $cur;

$buf .= " $l";
next unless $buf =~ /;/;              # statement not finished yet
my $stmt = $buf; $buf = "";

Three properties earn their place:

$cur is the enclosing module, so every instance is recorded with its parent. That one variable is what makes Chapter 20.4's hierarchy map possible, and the measurement already uses it: counter_bit 3, counter_top 15.

The buffer is cleared at module and endmodule. A statement cannot span a module boundary, so an unterminated buffer at the end of a module is a syntax error in the netlist, not something to carry forward.

A keyword check rejects declarations. input clk; and wire n10; match the shape "word word" perfectly well. Without %IS_KW, every declaration becomes an instance — Chapter 19.5 needed the same guard for the same reason.

4. What Breaks A Line Scan

Four hazards, in the order they will bite.

A block comment spanning lines. The scan has to carry a flag across iterations, because a /* on one line and its */ three lines later means everything between is not code. Scan D tracks $in_block for this — Chapter 18.7's state machine, with two states.

A comment on the same line as code. BUFFD1 U26 ( .A(rst_n), .Z() ); // output pin unconnected is an instantiation and a comment. Strip the comment, keep the statement.

The module header's port list. module counter_top ( opens a parenthesis that closes several lines later with );. If instance accumulation starts before the module line is consumed, the header itself looks like a statement ending in ; whose shape is "word word ( ... )" — an instance called counter_top. Scan D avoids it by handling the module line first and next-ing.

Directives. `timescale, `celldefine, `default_nettype — lines starting with a backtick that are neither declarations nor instances. They are what broke scan C, and in the line-oriented scan they fall through harmlessly because they do not match the instance shape.

And the honest limit, which is Chapter 20.2's entire subject: this is a scan, not a parser. It works because a flat synthesised netlist is regular, machine-generated text. It is not a general Verilog reader, and the next chapter measures exactly where the difference starts to matter.

5. What To Remember

  • A flat synthesised netlist is extremely regular text — cell type, instance name, connection list, repeated.
  • Net names are generated and meaningless, and they change on every resynthesis.
  • Nothing in the syntax distinguishes a library cell from a sub-module instance — only the library does.
  • The fixture is hand-written and verified: it elaborates and simulates (count=12, PASSED). Nothing here is presented as tool output.
  • Measured: a single-line instance pattern found 17 of 18. The miss is the multi-line form, which tools emit for long connection lists.
  • Stripping comments fixed nothing here — a defensive step for a hazard you do not have changes no numbers.
  • Measured: joining statements on ; found all 18 instances and reported ZERO modules — `timescale precedes module in the joined statement, so the anchor fails.
  • A statement is not a line. The two questions need two granularities.
  • module/endmodule are reliably line-oriented; an instantiation is a statement terminated by ; (18.6).
  • The hybrid scan does both, and it is what the rest of Module 20 uses.
  • Track the enclosing module in one variable — that is what makes the hierarchy map (20.4) possible.
  • Clear the buffer at module and endmodule: a statement cannot cross a module boundary.
  • A keyword deny-list is required, or wire n10; becomes an instance. It fails open, which is right here because cell types are open-ended and declaration keywords are not.
  • Four hazards: block comments across lines, trailing comments, the module header's own port list, and backtick directives.
  • The module header looks exactly like an instance if you let it reach the statement matcher.
  • This is a scan, not a parser — which is the subject of 20.2.

Chapter 20.2 draws the boundary. The scan built here is reliable on the netlist a synthesis tool emits, and there are specific, nameable constructs that defeat it entirely — worth knowing precisely, because the failure is silent.

Where this fits

Part of the Perl curriculum.