Skip to content
VLSI Mentor

Perl Part II · Module 20 · Netlist Processing

Netlist QC Checks That Catch Real Problems

The pieces built across this module — the scan, the census, the hierarchy, the connectivity — become useful when they are wired into a small set of checks that run without being asked. The hard part is not writing the checks; it is writing checks a reviewer will still be reading in six months. This chapter builds six, measures them against a good netlist and a deliberately broken one, compares them against what the compiler itself catches, and spends its central section on the two false-positive bugs that were in the checker when it was first run.

Advanced17 min readPerlPart IINetlists

Part II · Module 20 · Chapter 20.7 · Netlist QC Checks That Catch Real Problems

1. Six Checks, Three Netlists

The checks run on three inputs: the netlist used throughout this module, a version with deliberate defects, and a resynthesised version compared against the original.

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

my @FAMILY = (
  [ qr/^DF/, "flop" ], [ qr/^CKLN/, "clock gate" ], [ qr/^CK/, "clock buffer" ],
  [ qr/^(BUFF|INV)/, "buffer/inv" ],
  [ qr/^(AN|ND|OR|NR|XOR|XNR|MUX|AO|OA)/, "combinational" ],
  [ qr/^(FILL|TIEHI|TIELO|ANTENNA|DCAP)/, "physical-only" ],
);
sub family { my ($t) = @_; for my $f (@FAMILY) { return $f->[1] if $t =~ $f->[0] } return undef }

sub ports_of {
    my ($conns) = @_;
    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;
}

sub read_netlist {
    my ($file) = @_;
    open my $fh, "<", $file or die "$file: $!";
    my (@mods, @inst, $cur, $buf, $in_block, %ports);
    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+)/) { push @mods, $1; $cur = $1; $buf = ""; next }
        # a module PORT is not a dangling net: its other end is outside the
        # module. Without this the "goes nowhere" check fires on every port.
        if (defined $cur && $l =~ /^\s*(?:input|output|inout)\b(.*)$/) {
            my $rest = $1;
            $rest =~ s/\[[^\]]*\]//g;              # drop a bus range
            $ports{$cur}{$_} = 1 for ($rest =~ /(\w+)/g);
            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 (\@mods, \@inst, \%ports);
}

# ---- the checks. Each returns a list of [SEVERITY, message].
sub qc {
    my ($file, $baseline) = @_;
    my ($mods, $inst, $ports) = read_netlist($file);
    my %IS_MODULE = map { $_ => 1 } @$mods;
    my @f;

    my %count;
    $count{ $_->{type} }++ for grep { !$IS_MODULE{ $_->{type} } } @$inst;

    # 1 -- a cell type no family matches
    for my $t (sort grep { !defined family($_) } keys %count) {
        push @f, ["ERROR", "unrecognised cell type '$t' x$count{$t} -- not in the family table"];
    }
    # 2 -- physical-only cells in a netlist meant to be functional
    for my $t (sort grep { (family($_) // "") eq "physical-only" } keys %count) {
        push @f, ["WARN", "physical-only cell '$t' x$count{$t} in a functional netlist"];
    }
    # 3 -- unconnected pins
    my (%net, @open);
    for my $i (@$inst) {
        for my $p (ports_of($i->{conns})) {
            my ($port, $n) = @$p;
            if ($n eq "") { push @open, "$i->{name}.$port ($i->{type})"; next }
            next if $n =~ /^\d+'[bdho]/;              # a constant is not a net
            push @{ $net{"$i->{parent}:$n"} }, "$i->{name}.$port";
        }
    }
    push @f, ["WARN", "unconnected pin $_"] for @open;

    # 4 -- a net touched exactly once: it goes nowhere
    for my $k (sort keys %net) {
        next unless @{ $net{$k} } == 1;
        my ($mod, $n) = split /:/, $k, 2;
        (my $base = $n) =~ s/\[[^\]]*\]$//;      # count[0] -> count
        next if $ports->{$mod} && $ports->{$mod}{$base};
        push @f, ["WARN", "net '$n' in $mod touches only $net{$k}[0] -- goes nowhere"];
    }
    # 5 -- shape: a netlist with no flops at all
    my $flops = 0; $flops += $count{$_} for grep { (family($_) // "") eq "flop" } keys %count;
    push @f, ["NOTE", "no flops found -- combinational block, or the wrong file?"]
        unless $flops;

    # 6 -- instance-count delta against a baseline netlist
    if (defined $baseline) {
        my ($bmods, $binst) = read_netlist($baseline);
        my %B_IS_MODULE = map { $_ => 1 } @$bmods;
        my %bcount;
        # the SAME filter as %count above, or a sub-module shows as a delta
        $bcount{ $_->{type} }++ for grep { !$B_IS_MODULE{ $_->{type} } } @$binst;
        my %all = map { $_ => 1 } (keys %count, keys %bcount);
        for my $t (sort keys %all) {
            my ($was, $now) = ($bcount{$t} // 0, $count{$t} // 0);
            next if $was == $now;
            push @f, ["NOTE", sprintf("count delta %-10s %d -> %d (%+d)", $t, $was, $now, $now - $was)];
        }
    }
    return @f;
}

sub report {
    my ($label, @f) = @_;
    printf "\n==== %s ====\n", $label;
    unless (@f) { print "  no findings\n"; return 0 }
    my %by;
    push @{ $by{ $_->[0] } }, $_->[1] for @f;
    for my $sev (qw(ERROR WARN NOTE)) {
        next unless $by{$sev};
        printf "  %s (%d)\n", $sev, scalar @{ $by{$sev} };
        printf "    %s\n", $_ for @{ $by{$sev} };
    }
    return scalar @{ $by{ERROR} || [] };
}

my $e1 = report("counter_syn.v  (the good netlist)",  qc("netlist/counter_syn.v"));
my $e2 = report("counter_bad.v  (deliberate defects)", qc("netlist/counter_bad.v"));
my $e3 = report("counter_syn_v2.v vs counter_syn.v (resynthesis delta)",
                qc("netlist/counter_syn_v2.v", "netlist/counter_syn.v"));

print "\n==== is every check able to fire? ====\n";
my @all = (qc("netlist/counter_syn.v"), qc("netlist/counter_bad.v"),
           qc("netlist/counter_syn_v2.v", "netlist/counter_syn.v"));
for my $kind ("unrecognised cell type", "physical-only cell", "unconnected pin",
              "goes nowhere", "no flops found", "count delta") {
    my $n = grep { $_->[1] =~ /\Q$kind\E/ } @all;
    printf "  %-24s fired %d time(s)  %s\n", $kind, $n, ($n ? "" : "<-- NEVER FIRED");
}

printf "\nexit status would be: %d (ERROR findings on the good netlist)\n", $e1;
printf "and %d for the defective one\n", $e2;
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output

==== counter_syn.v  (the good netlist) ====
  WARN (4)
    physical-only cell 'FILL1' x1 in a functional netlist
    physical-only cell 'TIELO' x1 in a functional netlist
    unconnected pin U26.Z (BUFFD1)
    unconnected pin U27.ZN (TIELO)

==== counter_bad.v  (deliberate defects) ====
  ERROR (1)
    unrecognised cell type 'MYCELL_X9' x1 -- not in the family table
  WARN (5)
    physical-only cell 'FILL1' x1 in a functional netlist
    physical-only cell 'TIEHI' x1 in a functional netlist
    unconnected pin U2.ZN (INVD1)
    unconnected pin U3.A2 (ND2D1)
    net 'dangle' in counter_bad touches only T1.Z -- goes nowhere
  NOTE (1)
    no flops found -- combinational block, or the wrong file?

==== counter_syn_v2.v vs counter_syn.v (resynthesis delta) ====
  WARN (1)
    unconnected pin U28.ZN (INVD1)
  NOTE (3)
    count delta FILL1      1 -> 0 (-1)
    count delta INVD1      3 -> 4 (+1)
    count delta TIELO      1 -> 0 (-1)

==== is every check able to fire? ====
  unrecognised cell type   fired 1 time(s)  
  physical-only cell       fired 4 time(s)  
  unconnected pin          fired 5 time(s)  
  goes nowhere             fired 1 time(s)  
  no flops found           fired 1 time(s)  
  count delta              fired 3 time(s)  

exit status would be: 0 (ERROR findings on the good netlist)
and 1 for the defective one

Three results worth reading closely.

The good netlist produces 4 warnings and no errors — two physical-only cells and two unconnected pins, which are exactly the four things deliberately placed in it back in Chapter 20.1. A checker that reported zero on this netlist would be broken.

The defective netlist produces 1 error, 5 warnings and a note, including net 'dangle' touches only T1.Z — a tie cell driving a net nothing reads.

Every check fired at least once across the three runs. That block at the end of the output is not decoration: a check that has never fired on any input you own is a check you have no evidence about.

2. What The Compiler Catches, And What It Does Not

Before automating anything, it is worth knowing which problems the toolchain already finds, because duplicating it is wasted work and assuming it is reckless.

Measured on the defective netlist:

Without the cell model, the compiler catches the unknown cell type.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
netlist/counter_bad.v:12: error: Unknown module type: MYCELL_X9
rc=2

A clear, actionable error — but only if you have the library. Being handed a netlist without its library is the normal case, and then this error does not exist.

With the model supplied, the compiler reports nothing at all. rc=0, and searching its output for the unconnected input, the filler cell and the dangling net finds zero mentions of any of them.

So the division of labour is precise:

problemcompilerQC script
undefined cell typeyes, with the libraryyes, without it
unconnected input pinnoyes
physical-only cell in a functional netlistnoyes
a net that goes nowherenoyes
cell count changed since last runnoyes

Everything except the first is invisible to the compiler. That is the entire argument for the script: not that it is better than the tool, but that it answers questions the tool has no opinion about.

3. Two False Positives In My Own Checker

This is the part of the chapter worth remembering, because the first run of the script above was much worse than the output shown.

The "goes nowhere" check fired 16 times, and 15 were wrong. It reported:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
net 'CDN' in counter_bit touches only state_reg.CDN -- goes nowhere
net 'clk' in counter_top touches only clk_gate.CP -- goes nowhere
net 'Q'   in counter_bit touches only state_reg.Q  -- goes nowhere

Every one of those is a module port. A port touching exactly one pin inside its module is completely normal — the other end of the net is outside, at the instantiation. The check was measuring "one connection in this module" and calling it "goes nowhere", and those are different statements.

The fix is to know the ports, which means parsing the declarations the scan had been skipping:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if (defined $cur && $l =~ /^\s*(?:input|output|inout)\b(.*)$/) {
    my $rest = $1;
    $rest =~ s/\[[^\]]*\]//g;              # drop a bus range
    $ports{$cur}{$_} = 1 for ($rest =~ /(\w+)/g);
    next;
}

and then skip them, allowing for a bus element:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
(my $base = $n) =~ s/\[[^\]]*\]$//;        # count[0] -> count
next if $ports->{$mod} && $ports->{$mod}{$base};

The good netlist went from 10 warnings to 4, and dangle became the only "goes nowhere" finding in the whole run — which is correct, because it is the only genuine one.

The delta check reported a change that had not happened. count delta counter_bit 4 -> 0 (-4) on a netlist where counter_bit is instantiated four times in both versions. The cause was an asymmetric comparison: the current count excluded sub-modules, and the baseline count did not, so every sub-module appeared to have vanished.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
my %B_IS_MODULE = map { $_ => 1 } @$bmods;
# the SAME filter as %count above, or a sub-module shows as a delta
$bcount{ $_->{type} }++ for grep { !$B_IS_MODULE{ $_->{type} } } @$binst;

4. The Flow, And Severity

Figure 20.7.1 — The QC flow, and where severity is decided

A six-step QC flow: scan the netlist into instances, build the cell census, invert it into connectivity, compare against the previous netlist's census, assign a severity to each finding, then report grouped by severity and exit on errors only.Read, check, classify, report1Scan the netlistinstances + ports2Census by typeand by family3Invert to netspins per net4Compare to the lastsame filter both sides5Assign severityin ONE place6Report and exiterrors only are blocking
The first three steps are the module's earlier chapters, reused unchanged. The checks themselves are cheap once the data exists, which is why the ordering matters more than the implementation: nothing can be checked before the netlist has been read, and the delta cannot be computed before the census exists. Severity is assigned at the end, in one place, so that what counts as blocking is a policy decision somebody can change without touching a check.

Severity is the design decision that makes the report usable, and three levels are enough:

ERROR — the netlist is not fit to use. An unrecognised cell type goes here, because it means either the library is wrong or the netlist is. The measured run gives 0 errors on the good netlist and 1 on the defective one, and that is the exit status.

WARN — a human should look. Unconnected pins, physical-only cells, a net that goes nowhere. All four findings on the good netlist are warnings, and all four are real and known.

NOTE — informational, for the record. Count deltas and shape observations. A resynthesis that changes the inverter count is not a problem; it is a fact worth having in the log when somebody asks why the area moved.

And the exit status follows Chapter 15.4's three values, exactly as Chapter 19.7 did for filelists:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
exit 0 if no errors;          # usable
exit 1 if ERROR findings;     # the netlist is wrong
exit 3 if the netlist could not be read at all;

Errors only are blocking. A warning that stops the flow becomes an error somebody disables, and then the real errors are disabled with it.

5. The Delta Check

The most useful check here is the one that needs no judgement about what is right — only that something changed.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
count delta FILL1      1 -> 0 (-1)
count delta INVD1      3 -> 4 (+1)
count delta TIELO      1 -> 0 (-1)

Three facts about a resynthesis, obtained by comparing two censuses. None is a problem. All three are worth knowing, and the third is the kind of thing that answers a question a week later: the tie cell is gone because the pin it drove is now driven by the new inverter.

Two rules for making a delta check trustworthy:

The same filter on both sides. This is where my own bug was, and it is the easiest mistake to make because the two counts are computed in different places. If the current census excludes sub-modules, the baseline must too.

Report the direction and the magnitude, never a verdict. 3 -> 4 (+1) is a fact. "INVD1 count increased unacceptably" is a policy, and policy belongs with whoever owns the design, not in the script. Chapter 18.8's rule about reporting what you measured rather than what somebody wanted to know.

One thing a delta check cannot do, worth stating because it is tempting: it cannot tell you whether the two netlists are the same design. Net names change on every resynthesis (Chapter 20.1), instance names change, and two netlists of the same design can differ in thousands of lines while being functionally identical. Equivalence is a formal-verification question, and a cell-count delta is a smoke test in front of it — a useful one, and not a substitute.

6. What To Remember

  • Six checks worth automating: unrecognised cell types, physical-only cells, unconnected pins, nets that go nowhere, shape checks, and count deltas.
  • Measured: the good netlist gives 4 warnings and 0 errors — the four defects deliberately placed in it.
  • A checker that reports zero on a netlist with known defects is broken.
  • Measured: every check fired at least once across three netlists. A check that has never fired is a check you have no evidence about.
  • The compiler catches an undefined cell type — but only with the library, and being handed a netlist without its library is normal.
  • With the library, the compiler says nothing about an unconnected input, a filler in a functional netlist, or a dangling net — measured as 0 mentions.
  • That is the division of labour: the script answers what the tool has no opinion about.
  • My own "goes nowhere" check fired 16 times with 15 false positives — every module port looked like a dangling net.
  • A port touching one pin inside its module is normal; the other end is outside. Parse input/output/inout and exclude them, allowing for bus elements.
  • The good netlist went from 10 warnings to 4 after that fix.
  • My delta check reported counter_bit 4 -> 0 on an unchanged netlist — the current count filtered sub-modules and the baseline did not.
  • Use the same filter on both sides of any comparison.
  • A check that fires on correct input is worse than no check — 12 bogus findings in 16 means nobody reads the report, including the 4 real ones.
  • Both bugs were found by auditing the output line by line, not by a failure; both runs exited cleanly.
  • Before trusting a new check, justify every finding on a netlist you believe is good.
  • Three severities: ERROR (not fit to use), WARN (a human should look), NOTE (for the record).
  • Assign severity in one place, so policy can change without touching a check.
  • Errors only are blocking — a blocking warning becomes a disabled warning, and takes the errors with it.
  • A delta reports direction and magnitude, never a verdict (18.8).
  • A count delta cannot prove two netlists are the same design — names change on every resynthesis; equivalence is a formal question, and the delta is a smoke test in front of it.

That closes Module 20. You can take a synthesised netlist and read it honestly: scan instances across the multi-line forms, name precisely which legal constructs defeat the scan and why the failures are silent, pull apart a connection list whose commas are not all separators, build the hierarchy and get the elaborated count right — verified against the simulator itself — classify cells against a library-specific table that reports what it does not recognise, map connectivity at one level while knowing the two things the text cannot say, and wire the lot into checks whose false positives you have already hunted down.

Where this fits

Part of the Perl curriculum.