Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
hierarchy.pl
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";
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
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

A depth-indexed stack: index zero holds the top block, index one holds the current second-level block, index two holds the current third-level block. A row at depth two reads its parent from index one and then writes its own name into index two, and the array is truncated when a shallower row arrives.stack[0]chip_topstack[1]core0, then core1stack[2]bit0 to bit3a row at depth 2reads stack[1]then writes stack[2]for deeper rowsshallower row?truncate the array12
An array indexed by depth is the whole mechanism. Reading a row at depth two means its parent is whatever name is sitting at index one, because that is the most recent row shallower than it. Writing the name into index two then makes it available as the parent of any deeper row that follows. Truncating the array on the way back up is what prevents a stale name from a previous branch being handed out as a parent, which is the one bug this structure can still have.

Four lines carry it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$#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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

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

Every 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
own = row area - sum of its immediate children's areas

Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
the OWN areas sum to 69.6920, and the top's inclusive area is 69.6920 -> agree

The 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
depth 3 deep  parent=UNDEF <- the level between was never seen

A 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 write stack[depth].
  • Truncate with $#stack = depth - 1 BEFORE 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_top owns 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.