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
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";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:
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:
if (defined $pending && $l =~ $FIELDS_ONLY) { ... } # the continuation
if ($l =~ $NAME_ONLY) { $pending = $1; next } # the name aloneA 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:
my @FAMILY = (
[ qr/^DF/i, "flop" ],
[ qr/^SDFF/i, "scan flop" ],
...
);and the measured families in the real report:
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.5800Four 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:
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:
cells: summed 23, area report says 23 -> agree
area: summed 34.5800, area report says 34.5800 -> agreeTwo 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/$4against a three-group pattern and added an area as a count. - The only evidence was
Use of uninitialized value $4on 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.
^SDFFmust 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.
