Skip to content
VLSI Mentor

Perl Part II · Module 22 · Synthesis Report Parsing

Per-Cell-Type Counts

The cells report is the friendliest file in the flow: one row per cell type, with a count and an area already computed. That makes the parsing look trivial, and it is — right up to the three kinds of row that a reasonable pattern silently skips. This chapter measures those, then turns the list of types into the handful of families a reviewer actually asks about, where the order of the classification rules decides whether a whole category of cell is visible at all.

Intermediate12 min readPerlPart IISynthesis Reports

Part II · Module 22 · Chapter 22.3 · Per-Cell-Type Counts

1. Two Reports, Two Row Patterns

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

# counter_syn.cells.rpt is generated from the real Module 20 netlist.
# awkward.cells.rpt is the same report with three realistic awkward rows
# added: a hyphen in a name, a dot in a name, and a name long enough that
# the tool wrapped the row onto two lines.
my %AREA;
open my $al, "<", "lib/areas.txt" or die $!;
while (my $l = <$al>) { next if $l =~ /^\s*#/; $AREA{$1} = $2 if $l =~ /^(\w+)\s+([\d.]+)/ }
close $al;

# ---- A: the obvious row pattern
my $ROW_A = qr/^(\w+)\s+\S+\s+([\d.]+)\s+(\d+)\s+([\d.]+)\s*$/;
# ---- B: allow the characters a cell name can actually contain, and handle a
# row whose fields wrapped onto the following line
my $ROW_B = qr/^([A-Za-z][\w.\-\/\[\]]*)\s+\S+\s+([\d.]+)\s+(\d+)\s+([\d.]+)\s*$/;
my $NAME_ONLY = qr/^([A-Za-z][\w.\-\/\[\]]*)\s*$/;
my $FIELDS_ONLY = qr/^\s+\S+\s+([\d.]+)\s+(\d+)\s+([\d.]+)\s*$/;

sub parse {
    my ($file, $mode) = @_;
    open my $fh, "<", $file or die "$file: $!";
    my (%count, %area, $pending);
    while (my $l = <$fh>) {
        chomp $l;
        next if $l =~ /^\s*(?:\*|Report|Design|Generated|-+\s*$|$)/;
        next if $l =~ /^(?:Reference|Total)\b/;
        if ($mode eq "B") {
            if (defined $pending && $l =~ $FIELDS_ONLY) {
                # $FIELDS_ONLY has THREE groups (unit area, count, total), so
                # the count is $2 and the total area is $3. Using $3/$4 here --
                # the numbers from the FULL row pattern -- silently added an
                # area as a count and warned about undef $4 on stderr.
                $count{$pending} += $2;
                $area{$pending}  += $3;
                undef $pending;
                next;
            }
            if ($l =~ $NAME_ONLY) { $pending = $1; next }
        }
        my $pat = ($mode eq "A") ? $ROW_A : $ROW_B;
        next unless $l =~ $pat;
        $count{$1} += $3;
        $area{$1}  += $4;
    }
    close $fh;
    return (\%count, \%area);
}

my @REPORTS = ("reports/counter_syn.cells.rpt", "reports/awkward.cells.rpt");
my %TRUTH_TYPES = ("reports/counter_syn.cells.rpt" => 9,
                   "reports/awkward.cells.rpt"     => 12);

printf "%-22s %-10s %-10s %-10s %s\n", "REPORT", "TYPES(A)", "TYPES(B)", "truth", "verdict";
for my $r (@REPORTS) {
    my ($short) = $r =~ m{([^/]+)\.cells\.rpt$};
    my ($ca) = parse($r, "A");
    my ($cb) = parse($r, "B");
    my $t = $TRUTH_TYPES{$r};
    printf "%-22s %-10d %-10d %-10d %s\n", $short, scalar keys %$ca, scalar keys %$cb, $t,
        (scalar(keys %$cb) == $t ? (scalar(keys %$ca) == $t ? "both ok" : "only B correct")
                                 : "BOTH WRONG");
}

print "\nwhich rows does A miss in the awkward report?\n";
my ($ca) = parse("reports/awkward.cells.rpt", "A");
my ($cb) = parse("reports/awkward.cells.rpt", "B");
for my $t (sort keys %$cb) { printf "  %-32s %s\n", $t, ($ca->{$t} ? "found by both" : "MISSED by A") }

# ---- families (20.5's table, as data)
my @FAMILY = (
  [ qr/^DF/i,            "flop"          ],
  [ qr/^SDFF/i,          "scan 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 }

my ($count, $area) = parse("reports/counter_syn.cells.rpt", "B");
my (%fc, %fa);
for my $t (keys %$count) {
    my $f = family($t) // "*** UNKNOWN ***";
    $fc{$f} += $count->{$t};
    $fa{$f} += $area->{$t};
}
print "\nfamilies in the real report:\n";
printf "%-16s %8s %12s\n", "FAMILY", "CELLS", "AREA";
printf "%-16s %8d %12.4f\n", $_, $fc{$_}, $fa{$_} for sort keys %fc;
my ($tc, $ta) = (0, 0);
$tc += $_ for values %fc; $ta += $_ for values %fa;
printf "%-16s %8d %12.4f\n", "TOTAL", $tc, $ta;

print "\ncross-check against the area report (22.1's habit):\n";
open my $ar, "<", "reports/counter_syn.area.rpt" or die $!;
my ($stated_cells, $stated_area);
while (my $l = <$ar>) {
    $stated_cells = $1 if $l =~ /^\s*Number of cells:\s+(\d+)/;
    $stated_area  = $1 if $l =~ /^\s*Total cell area:\s+([\d.]+)/;
}
close $ar;
printf "  cells: summed %d, area report says %s -> %s\n", $tc, $stated_cells,
    ($tc == $stated_cells ? "agree" : "DISAGREE");
printf "  area:  summed %.4f, area report says %.4f -> %s\n", $ta, $stated_area,
    (abs($ta - $stated_area) < 1e-6 ? "agree" : "DISAGREE");

print "\nand the SDFF row in the awkward report:\n";
printf "  SDFF.RPQ_X2 classifies as '%s'\n", family("SDFF.RPQ_X2") // "UNKNOWN";
print "  note the ^SDFF rule sits BEFORE ^DF in the table; reversed, a scan\n";
print "  flop would be counted as an ordinary flop and the scan-chain review\n";
print "  would see nothing.\n";
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
REPORT                 TYPES(A)   TYPES(B)   truth      verdict
counter_syn            9          9          9          both ok
awkward                9          12         12         only B correct

which rows does A miss in the awkward report?
  AN2D1                            found by both
  BUFFD1                           found by both
  CKBD1                            found by both
  CKLNQD1                          found by both
  DFCNQD1                          found by both
  FILL1                            found by both
  INVD1                            found by both
  MUX2-D1                          MISSED by A
  SDFF.RPQ_X2                      MISSED by A
  TIELO                            found by both
  VERY_LONG_CELL_NAME_THAT_WRAPS   MISSED by A
  XOR2D1                           found by both

families in the real report:
FAMILY              CELLS         AREA
buffer/inv             10       5.5860
clock buffer            1       1.0640
clock gate              1       3.1920
combinational           5       6.9160
flop                    4      17.0240
physical-only           2       0.7980
TOTAL                  23      34.5800

cross-check against the area report (22.1's habit):
  cells: summed 23, area report says 23 -> agree
  area:  summed 34.5800, area report says 34.5800 -> agree

and the SDFF row in the awkward report:
  SDFF.RPQ_X2 classifies as 'scan flop'
  note the ^SDFF rule sits BEFORE ^DF in the table; reversed, a scan
  flop would be counted as an ordinary flop and the scan-chain review
  would see nothing.

Both patterns read the clean report correctly. On the awkward one, the obvious pattern reads 9 of 12 rows. The three it drops are named in the output, and each is a different problem.

2. What A Cell Name Can Contain

^(\w+) means letters, digits and underscore. Real library cell names contain more than that:

MUX2-D1 — a hyphen. Common in libraries that separate function from drive strength with a dash.

SDFF.RPQ_X2 — a dot. Common where the name encodes a variant hierarchy.

So the character class has to be widened to what a name can actually hold:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
my $ROW_B = qr/^([A-Za-z][\w.\-\/\[\]]*)\s+\S+\s+([\d.]+)\s+(\d+)\s+([\d.]+)\s*$/;

Note it still starts with a letter — that is what keeps the separator rows (-----) and the numeric columns from matching. Widening a class is not the same as removing the anchor, and Chapter 11.8's point holds: the anchor is doing the work, the class is just permissive enough.

The third row is a different problem entirely. VERY_LONG_CELL_NAME_THAT_WRAPS is too long for its column, so the tool emitted the name on one line and the numbers on the next. No single-line pattern can read that, however permissive — it is Chapter 18.6's multi-line record, appearing in a table:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if (defined $pending && $l =~ $FIELDS_ONLY) { ... }   # the continuation
if ($l =~ $NAME_ONLY) { $pending = $1; next }         # the name alone

A name-only line sets $pending; the next fields-only line completes the row. Two patterns and one state variable — Chapter 18.7's state machine at its smallest useful size.

3. Families, And The Ordering That Matters

Classification is Chapter 20.5's table of pattern to family, as data:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
my @FAMILY = (
  [ qr/^DF/i,   "flop"      ],
  [ qr/^SDFF/i, "scan flop" ],
  ...
);

and the measured families in the real report:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
buffer/inv       10 cells    5.5860
clock buffer      1          1.0640
clock gate        1          3.1920
combinational     5          6.9160
flop              4         17.0240
physical-only     2          0.7980
TOTAL            23         34.5800

Four flops are 49% of the cell area in 17% of the cells, which is the kind of fact a family roll-up exists to show and a per-type list buries.

Then the ordering. As written above the table is wrong, and the measurement shows the right version:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
SDFF.RPQ_X2 classifies as 'scan flop'

^SDFF must come before ^DF, because SDFF starts with an S — but with /i and a ^DF rule placed first, a library spelling like DFFSCAN would land in flop. The consequence is specific: a scan flop counted as an ordinary flop makes a scan-chain review see nothing, because the number it is looking for is hidden inside a larger, expected number.

This is the third time this module and its neighbours have hit the same rule — Chapter 20.5's ^CKLN before ^CK, Chapter 21.8's TIMEOUT before CRASH, and here. When one pattern's input is a subset of another's, the specific one goes first, and every instance of getting it wrong is silent.

4. The Cross-Check

Chapter 22.1 established the habit and this chapter is where it pays:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cells: summed 23, area report says 23 -> agree
area:  summed 34.5800, area report says 34.5800 -> agree

Two independent files agree on both quantities. The cells report's rows were summed; the area report's totals were parsed by Chapter 22.2's spelling table. Neither computation shares code with the other.

What a disagreement would mean, in the order worth checking:

Cells low, area low — rows are being missed. Exactly the failure section 1 measured, and the most likely cause by a wide margin.

Cells agree, area low — a row was read but one of its numbers was not. A unit-area column that is undefined for a black box, or a value in scientific notation that [\d.]+ does not match.

Cells high — a non-row line matched. The Total line is the usual culprit, which is why the parser skips ^(?:Reference|Total)\b explicitly.

And a note on what the sum cannot check: it cannot tell you the report is complete. A truncated cells report has fewer rows and a smaller total, and the two agree with each other perfectly. Chapter 18.9's three states apply to a report as much as to a log — and the area report's own Number of cells is the outside evidence that catches it.

5. What To Remember

  • One row per cell type, with the count and area already computed — the friendliest file in the flow.
  • Measured: the obvious row pattern reads 9 of 12 rows on a report with realistic awkward names.
  • \w+ cannot match a hyphen or a dot, and both occur in real cell names (MUX2-D1, SDFF.RPQ_X2).
  • Widen the class, keep the anchor: ^([A-Za-z][\w.\-\/\[\]]*) still rejects separator and numeric lines (11.8).
  • A long name makes the tool WRAP the row onto two lines, and no single-line pattern can read it.
  • Handle the wrap as a multi-line record (18.6): a name-only line, then a fields-only line, with one state variable (18.7).
  • Two patterns for one conceptual row almost never share capture numbering — mine used $3/$4 against a three-group pattern and added an area as a count.
  • The only evidence was Use of uninitialized value $4 on stderr while stdout looked plausible (14.2, 20.3).
  • Group types into families with a table, as data (20.5).
  • Measured: 4 flops are 49% of the cell area in 17% of the cells — the fact a family roll-up exists to show.
  • ^SDFF must precede ^DF, or a scan flop is counted as an ordinary flop and a scan-chain review sees nothing.
  • When one pattern's input is a subset of another's, the specific one goes first — third instance of this rule in three modules, and every failure is silent.
  • Cross-check the sums against the area report: measured 23 cells and 34.5800 area, both agreeing.
  • Diagnose a disagreement by direction: both low means missed rows; area low alone means a field not read; cells high means a non-row line matched.
  • The sum cannot detect a truncated report — fewer rows and a smaller total agree perfectly. The area report's own cell count is the outside evidence (18.9).

Chapter 22.4 takes the third file, where the structure is in the whitespace. A hierarchy report encodes the parent of every row in its indentation, and reconstructing the tree from that is a different problem from anything in this module so far.

Where this fits

Part of the Perl curriculum.