Perl Part II · Module 22 · Synthesis Report Parsing
Walking report_hierarchy
A hierarchy report is the one synthesis output whose structure is carried in whitespace: each row's parent is whichever row above it is indented one level less. Reconstructing that is a short exercise with an array indexed by depth. The reason the chapter is worth its length is the number in the rows — because the obvious thing to do with a column of areas is add it up, and on this report that answer is wrong by a factor that grows with the depth of your design.
Intermediate11 min readPerlPart IISynthesis Reports
Part II · Module 22 · Chapter 22.4 · Walking report_hierarchy
1. Eleven Rows, Parents From Whitespace
use strict;
use warnings;
# chip_flat.hier.rpt is generated from a REAL three-level netlist
# (chip_top -> 2 x counter_top -> 4 x counter_bit) that Icarus elaborates and
# simulates. The structure is in the INDENTATION, which is the whole problem.
my $RPT = "reports/chip_flat.hier.rpt";
my $ROW = qr/^(\s*)(\S+)\s+(\d+)\s+([\d.]+)\s*$/;
my @rows;
open my $fh, "<", $RPT or die "$RPT: $!";
while (my $l = <$fh>) {
chomp $l;
next if $l =~ /^\s*(?:\*|Report|Design|Generated|-+\s*$|$)/;
next if $l =~ /^Hierarchical/;
next if $l =~ /^\d+\s*$/; # the trailing page marker
next unless $l =~ $ROW;
my ($indent, $name, $cells, $area) = ($1, $2, $3, $4);
push @rows, { depth => length($indent) / 2, name => $name,
cells => $cells, area => $area };
}
close $fh;
printf "%d rows parsed from %s\n\n", scalar @rows, $RPT;
# ---- reconstruct the parent of each row with a DEPTH STACK
my @stack;
for my $r (@rows) {
$#stack = $r->{depth} - 1 if @stack > $r->{depth}; # pop to this depth
$r->{parent} = $r->{depth} ? $stack[$r->{depth} - 1] : "-";
$stack[$r->{depth}] = $r->{name};
$r->{path} = join("/", @stack[0 .. $r->{depth}]);
}
printf "%-7s %-10s %-10s %-7s %-10s %s\n", "DEPTH", "NAME", "PARENT", "CELLS", "AREA", "path";
printf "%-7d %-10s %-10s %-7d %-10.4f %s\n",
$_->{depth}, $_->{name}, $_->{parent}, $_->{cells}, $_->{area}, $_->{path} for @rows;
# ---- THE TRAP: are these areas inclusive or exclusive?
my ($naive_cells, $naive_area) = (0, 0);
$naive_cells += $_->{cells}, $naive_area += $_->{area} for @rows;
my ($top) = grep { $_->{depth} == 0 } @rows;
printf "\nsumming EVERY row: %d cells, %.4f area\n", $naive_cells, $naive_area;
printf "the top row says: %d cells, %.4f area\n", $top->{cells}, $top->{area};
printf "the naive sum is %.2fx too big\n", $naive_area / $top->{area};
print "because each row's figure INCLUDES its children. The roll-up is\n";
print "inclusive, so adding the rows counts the leaves once per ancestor.\n";
# ---- the EXCLUSIVE (own) area of each block, which is what people want
my %child_sum;
for my $r (@rows) { $child_sum{ $r->{path} } = 0 }
for my $r (@rows) {
next unless $r->{depth};
my $ppath = join("/", (split m{/}, $r->{path})[0 .. $r->{depth} - 1]);
$child_sum{$ppath} += $r->{area};
}
print "\nown (exclusive) area, = row area minus the sum of its children:\n";
printf "%-22s %-12s %-12s %s\n", "PATH", "INCLUSIVE", "CHILDREN", "OWN";
for my $r (@rows) {
my $own = $r->{area} - ($child_sum{ $r->{path} } // 0);
printf "%-22s %-12.4f %-12.4f %.4f\n", $r->{path}, $r->{area},
($child_sum{ $r->{path} } // 0), $own;
}
my $own_total = 0;
for my $r (@rows) { $own_total += $r->{area} - ($child_sum{ $r->{path} } // 0) }
printf "\nthe OWN areas sum to %.4f, and the top's inclusive area is %.4f -> %s\n",
$own_total, $top->{area},
(abs($own_total - $top->{area}) < 1e-6 ? "agree" : "DISAGREE");
print "which is the check that proves the exclusive split is right.\n";
# ---- a depth JUMP, which a naive parent-tracker gets wrong
print "\nwhat if a report skips a level (depth jumps by 2)?\n";
my @fake = ({depth=>0,name=>"top"}, {depth=>1,name=>"a"}, {depth=>3,name=>"deep"});
my @st2;
for my $r (@fake) {
$#st2 = $r->{depth} - 1 if @st2 > $r->{depth};
my $p = $r->{depth} ? $st2[$r->{depth} - 1] : "-";
$st2[$r->{depth}] = $r->{name};
printf " depth %d %-5s parent=%s\n", $r->{depth}, $r->{name},
(defined $p ? $p : "UNDEF <- the level between was never seen");
}
print " a stack indexed by depth reports UNDEF rather than silently\n";
print " attaching 'deep' to 'a'. Detect it and report the report as odd.\n";11 rows parsed from reports/chip_flat.hier.rpt
DEPTH NAME PARENT CELLS AREA path
0 chip_top - 47 69.6920 chip_top
1 core0 chip_top 23 34.5800 chip_top/core0
2 bit0 core0 3 5.3200 chip_top/core0/bit0
2 bit1 core0 3 5.3200 chip_top/core0/bit1
2 bit2 core0 3 5.3200 chip_top/core0/bit2
2 bit3 core0 3 5.3200 chip_top/core0/bit3
1 core1 chip_top 23 34.5800 chip_top/core1
2 bit0 core1 3 5.3200 chip_top/core1/bit0
2 bit1 core1 3 5.3200 chip_top/core1/bit1
2 bit2 core1 3 5.3200 chip_top/core1/bit2
2 bit3 core1 3 5.3200 chip_top/core1/bit3
summing EVERY row: 117 cells, 181.4120 area
the top row says: 47 cells, 69.6920 area
the naive sum is 2.60x too big
because each row's figure INCLUDES its children. The roll-up is
inclusive, so adding the rows counts the leaves once per ancestor.
own (exclusive) area, = row area minus the sum of its children:
PATH INCLUSIVE CHILDREN OWN
chip_top 69.6920 69.1600 0.5320
chip_top/core0 34.5800 21.2800 13.3000
chip_top/core0/bit0 5.3200 0.0000 5.3200
chip_top/core0/bit1 5.3200 0.0000 5.3200
chip_top/core0/bit2 5.3200 0.0000 5.3200
chip_top/core0/bit3 5.3200 0.0000 5.3200
chip_top/core1 34.5800 21.2800 13.3000
chip_top/core1/bit0 5.3200 0.0000 5.3200
chip_top/core1/bit1 5.3200 0.0000 5.3200
chip_top/core1/bit2 5.3200 0.0000 5.3200
chip_top/core1/bit3 5.3200 0.0000 5.3200
the OWN areas sum to 69.6920, and the top's inclusive area is 69.6920 -> agree
which is the check that proves the exclusive split is right.
what if a report skips a level (depth jumps by 2)?
depth 0 top parent=-
depth 1 a parent=top
depth 3 deep parent=UNDEF <- the level between was never seen
a stack indexed by depth reports UNDEF rather than silently
attaching 'deep' to 'a'. Detect it and report the report as odd.Eleven rows, and the first table shows every parent correctly recovered: core0 and core1 under chip_top, four bit blocks under each core. Nothing in the text says "parent" — it is all in the leading spaces.
2. The Depth Stack
Figure 22.4.1 — Recovering the parent from the indentation
Four lines carry it:
$#stack = $r->{depth} - 1 if @stack > $r->{depth}; # pop back to this depth
$r->{parent} = $r->{depth} ? $stack[$r->{depth} - 1] : "-";
$stack[$r->{depth}] = $r->{name};
$r->{path} = join("/", @stack[0 .. $r->{depth}]);$#stack = N truncates the array (Chapter 5.5's last-index assignment), and it is the line that matters. Without it, core1's children would still see core0's names at deeper indices — a stale parent from a branch that has finished. The truncation happens before the parent is read, which is what makes the read correct.
The depth comes from the indent width divided by the step. Two spaces per level here:
push @rows, { depth => length($indent) / 2, ... };That division is an assumption about the report, and it is worth asserting rather than trusting: an indent that is not a multiple of the step means the step is wrong, and a non-integer depth will index the array in a way that silently produces nonsense.
The path is built for free from the stack slice, and it is what makes rows addressable — chip_top/core0/bit0 rather than four rows all called bit0. Section 4 needs it.
3. The Trap: The Areas Are Inclusive
Now the measurement that justifies the chapter:
summing EVERY row: 117 cells, 181.4120 area
the top row says: 47 cells, 69.6920 area
the naive sum is 2.60x too bigEvery row's figure already includes its children's. So adding the column counts each leaf cell once for every ancestor it has: a bit block's 5.32 is counted in its own row, in its core's row, and in chip_top's row.
Three consequences worth being precise about:
The factor grows with depth. Three levels gives 2.60x here. A real chip with eight or ten levels of hierarchy gives a much larger factor, and there is no fixed number to correct by — it depends on where the cells sit.
The wrong answer is plausible. 181.41 is not an absurd area. Nothing about it says "this is three times too big", which is Chapter 22.2's lesson about the false invariant arriving from the other side: the obvious arithmetic on a correct report gives a confident wrong number.
The top row is the total. If you want total area from this file, read the depth-zero row — or better, read the area report, which is the file whose subject is that number (Chapter 22.1).
4. Exclusive Area, And The Identity That Proves It
What people actually want from a hierarchy report is own area — how much each block contributes by itself:
own = row area - sum of its immediate children's areasMeasured:
PATH INCLUSIVE CHILDREN OWN
chip_top 69.6920 69.1600 0.5320
chip_top/core0 34.5800 21.2800 13.3000
chip_top/core0/bit0 5.3200 0.0000 5.3200chip_top owns 0.532 — one inverter — and everything else is inside its two cores. That is the shape of a real top level, and the inclusive column would have told you it was 69.69, which is true and useless for "where is the area".
Then the identity that makes the decomposition trustworthy:
the OWN areas sum to 69.6920, and the top's inclusive area is 69.6920 -> agreeThe exclusive areas must sum to the top's inclusive area, because every leaf is counted exactly once. It costs one loop, and unlike Chapter 22.2's false invariant this one is true by construction — which is the difference between a check derived from the data model and one derived from reading label names.
5. A Skipped Level
The last measurement is a report that jumps from depth 1 to depth 3:
depth 3 deep parent=UNDEF <- the level between was never seenA depth-indexed stack reports undef rather than silently attaching deep to a. That is the right failure: a parent that was never printed cannot be inferred, and guessing the nearest shallower row would produce a tree that looks complete and is wrong.
When does this happen on a real report? Two ways worth knowing:
The report was truncated or filtered. A tool asked for cells above an area threshold prints the blocks that pass and not their parents.
The indent step is not what you assumed. If a report uses three spaces per level and the parser divides by two, depths come out as 0, 1.5, 3 — and the array indexing goes wrong in a way that produces exactly this symptom. Which is why section 2 recommends asserting the indent is a multiple of the step.
Either way the response is the same: report the row and the gap, and do not build the tree. A partial tree presented as complete is worse than no tree, and Chapter 18.8's rule covers it — a parser that meets something it cannot resolve says so.
6. What To Remember
- A hierarchy report carries its structure in whitespace — nothing in the text names a parent.
- An array indexed by depth is the whole mechanism: read
stack[depth-1]as the parent, then writestack[depth]. - Truncate with
$#stack = depth - 1BEFORE reading the parent (5.5), or a stale name from a finished branch is handed out. - Depth is the indent width divided by the step — and that step is an assumption worth asserting.
- Build each row's path from the stack slice — it is what makes rows addressable.
- Measured: summing every row gives 181.4120 against a true 69.6920 — 2.60x too big.
- The areas are INCLUSIVE: each row already contains its children, so a leaf is counted once per ancestor.
- The factor grows with depth and there is no fixed correction — it depends on where the cells sit.
- The wrong answer is plausible (22.2's lesson from the other side).
- For a total, read the depth-zero row — or the area report, whose subject it is (22.1).
- Own area = row area minus the sum of immediate children.
chip_topowns 0.532 of 69.692. - The exclusive areas must sum to the top's inclusive area — measured exactly, and true by construction rather than inferred from labels.
- Key the child sums on PATH, not name — four rows are called
bit0(20.4's instance-versus-type point). - A skipped level must give
undef, not a guessed parent — a depth-indexed stack does this naturally. - A level gap means a filtered report or a wrong indent step — report the gap and do not build the tree (18.8).
Chapter 22.5 puts two runs side by side. With area, counts and hierarchy parsed, the question a review actually starts from is not what the numbers are but which of them moved.
Where this fits
Part of the Perl curriculum.
