Skip to content
VLSI Mentor

Perl Part I · Module 7 · References

Hashes of Hashes

A hash of hashes stores records by name — the block, then the field — and it is the shape a parsed tool report naturally takes, because tool output is usually a sequence of statements about named things. Building one is as economical as the previous chapter: a single assignment to $report{$block}{$field} creates both levels with no setup. What makes this chapter Advanced is not the notation but a consequence of that economy. The same mechanism that spares you the setup also means that reaching into the second level of a key that does not exist creates the first level as a side effect — and that happens even when all you did was ask whether something was there. A question that changes the data is worth understanding properly.

Advanced16 min readPerlPart IReferencesData Structures

Part I · Module 7 · Chapter 7.8 · Hashes of Hashes

1. Records Keyed by Name

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

my %report = (
    alu  => { slack => -0.12, paths => 1840, corner => "ss" },
    fifo => { slack =>  0.34, paths =>  920, corner => "ff" },
);

for my $blk (sort keys %report) {
    printf "%-5s slack %+6.2f  paths %5d  corner %s\n",
        $blk,
        $report{$blk}{slack},
        $report{$blk}{paths},
        $report{$blk}{corner};
}

printf "blocks : %d\n", scalar(keys %report);
printf "fields in alu : %d\n", scalar(keys %{ $report{alu} });
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
alu   slack  -0.12  paths  1840  corner ss
fifo  slack  +0.34  paths   920  corner ff
blocks : 2
fields in alu : 3

$report{$blk}{slack} is two lookups: the block, then the field. No arrow is needed between them, per Chapter 7.4 §3.

The two counts answer different questions and both get asked constantly:

QuestionExpression
how many blocksscalar(keys %report)
how many fields one block hasscalar(keys %{ $report{alu} })
one field$report{alu}{slack}
one block's whole record%{ $report{alu} }
every block namekeys %report
every field name of one blockkeys %{ $report{alu} }

The shape, drawn

A hash of hashes is one outer hash keyed by name, holding a record per key

Diagram showing an outer hash keyed by block name, each key holding a record hash, with two example two-level lookups%reportkeyed by blockaluslack paths cornerfifoslack paths cornerreport{alu}{slack}reaches -0.12report{fifo}{paths}reaches 920{alu}{fifo}{slack}{paths}12
The outer hash maps each block name to a reference to that block's record. A field is two named lookups: the first picks the record, the second picks the field inside it. Neither level has any inherent order, so both get sorted for output.

Compared with Chapter 7.6's table, the gain is that fields have names. $report{alu}{slack} says what it means; $table->[0][1] does not. The loss is ordering — neither level remembers the order anything arrived in, which §3 deals with.

2. Building One From Input

A single assignment creates both levels.

Azvya Education Pvt. Ltd.VLSI Mentor
hoh-build.pl
use strict;
use warnings;

my @lines = (
    "alu  slack -0.12",
    "alu  paths 1840",
    "fifo slack 0.34",
    "fifo paths 920",
    "alu  corner ss",
);

my %report;

for my $line (@lines) {
    my ($blk, $field, $value) = split ' ', $line;
    $report{$blk}{$field} = $value;
}

for my $blk (sort keys %report) {
    printf "%-5s : %s\n", $blk,
        join("  ", map { "$_=$report{$blk}{$_}" } sort keys %{ $report{$blk} });
}
printf "blocks %d\n", scalar(keys %report);
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
alu   : corner=ss  paths=1840  slack=-0.12
fifo  : paths=920  slack=0.34
blocks 2

$report{$blk}{$field} = $value; is the whole loop body. The first time a block is seen, the inner hash does not exist, and Perl creates it — the same autovivification that powered Chapter 7.7's push, and Chapter 7.9's subject.

Two things in the output are worth reading carefully:

  • The fields come out alphabetical, not in input order. alu shows corner, paths, slack although corner arrived last. The inner container is a hash; sort keys imposed the only order there is.
  • Blocks need not have the same fields. fifo has two, alu has three. Nothing pads, and nothing complains. That flexibility is useful for sparse tool output and dangerous when you assume a field is present — which is what §4 is about.

3. Walking Two Levels

Azvya Education Pvt. Ltd.VLSI Mentor
hoh-walk.pl
use strict;
use warnings;

my %report = (
    alu  => { slack => -0.12, paths => 1840 },
    fifo => { slack =>  0.34, paths =>  920 },
);

for my $blk (sort keys %report) {
    for my $field (sort keys %{ $report{$blk} }) {
        printf "%-5s %-6s %s\n", $blk, $field, $report{$blk}{$field};
    }
}

my @failing = grep { $report{$_}{slack} < 0 } sort keys %report;
printf "failing : %s\n", join(" ", @failing);

my $total = 0;
$total += $report{$_}{paths} for keys %report;
printf "total paths : %d\n", $total;
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
alu   paths  1840
alu   slack  -0.12
fifo  paths  920
fifo  slack  0.34
failing : alu
total paths : 2760

Both loops need sort, and the inner one needs sort keys %{ $report{$blk} } — the braces open the record so keys has a hash to work on.

Note the two different styles in the last few lines, and that the difference is deliberate:

  • grep { ... } sort keys %report sorts because the result is printed, so it must be reproducible.
  • $total += ... for keys %report does not sort, because addition does not care about order and sorting would be wasted work.

Sort when the order reaches a human or a diff. Do not sort when you are only accumulating.

4. Asking Can Create

This is the section that makes the chapter Advanced.

Azvya Education Pvt. Ltd.VLSI Mentor
hoh-exists.pl
use strict;
use warnings;

my %r = ( alu => { slack => -0.12 } );

printf "alu exists         : %s\n", (exists $r{alu}          ? "yes" : "no");
printf "alu.slack exists   : %s\n", (exists $r{alu}{slack}   ? "yes" : "no");
printf "alu.corner exists  : %s\n", (exists $r{alu}{corner}  ? "yes" : "no");
printf "fifo exists        : %s\n", (exists $r{fifo}         ? "yes" : "no");
printf "fifo.slack exists  : %s\n", (exists $r{fifo}{slack}  ? "yes" : "no");
printf "AND NOW fifo exists: %s\n", (exists $r{fifo}         ? "yes" : "no");
printf "top-level keys     : %d\n", scalar(keys %r);
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
alu exists         : yes
alu.slack exists   : yes
alu.corner exists  : no
fifo exists        : no
fifo.slack exists  : no
AND NOW fifo exists: yes
top-level keys     : 2

Read lines four, five and six in order.

  1. exists $r{fifo} — no. Correct; there is no fifo.
  2. exists $r{fifo}{slack} — no. Also correct as an answer.
  3. exists $r{fifo} — yes.

Nothing was assigned between them. The question in step 2 created $r{fifo}, and the top-level key count is now 2 rather than 1.

The mechanism is the one that has been helping all along. To evaluate $r{fifo}{slack}, Perl must first get something hash-like out of $r{fifo} to apply {slack} to. $r{fifo} sits in a position that is allowed to be assigned, so Perl puts an empty hash reference there. The exists then correctly reports that slack is not in that brand-new empty hash — and the hash stays.

5. Deleting at Either Level

Azvya Education Pvt. Ltd.VLSI Mentor
hoh-delete.pl
use strict;
use warnings;

my %r = (
    alu  => { slack => -0.12, paths => 1840 },
    fifo => { slack =>  0.34 },
);

delete $r{alu}{paths};
printf "alu fields after inner delete : %d  [%s]\n",
    scalar(keys %{ $r{alu} }), join(" ", sort keys %{ $r{alu} });

delete $r{fifo};
printf "top-level keys after delete    : %d  [%s]\n",
    scalar(keys %r), join(" ", sort keys %r);
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
alu fields after inner delete : 1  [slack]
top-level keys after delete    : 1  [alu]

delete $r{alu}{paths} removes one field; delete $r{fifo} removes a whole record. Both work as Chapter 6.3 describes, at whichever level you aim them.

Deleting the top-level key discards the inner hash as well — there is no other reference to it, so it goes. That is Chapter 7.2 §3's reference counting in reverse: the data lived while something referred to it, and now nothing does.

6. Missing Fields

Because records need not have the same fields, reading a field that is not there is a normal risk.

Azvya Education Pvt. Ltd.VLSI Mentor
hoh-missing.pl
use strict;
use warnings;

my %r = (
    alu  => { slack => -0.12, corner => "ss" },
    fifo => { slack =>  0.34 },
);

for my $blk (sort keys %r) {
    my $corner = $r{$blk}{corner} // "(none)";
    printf "%-5s corner %s\n", $blk, $corner;
}
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output — STDOUT
alu   corner ss
fifo  corner (none)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet

// supplies a default when the field is absent or undef, and it keeps STDERR clean. Drop it and the same program warns:

Azvya Education Pvt. Ltd.VLSI Mentor
Captured output — STDOUT, without the // default
alu   corner ss
fifo  corner
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output — STDERR, without the // default
Use of uninitialized value in printf at hoh-missing-nodefault.pl line 10.

A blank field and a warning, exit 0 — the same pattern Chapter 7.6 §4 measured for a missing array cell.

// is the right operator here rather than || because a field whose legitimate value is 0 must survive. Measured on a record whose slack genuinely is zero:

Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
raw value      : 0
with //        : 0
with ||        : (none)

|| replaced a real measurement with (none), because 0 is false. // asks only whether the value is defined, which is the question you meant. Chapter 6.4 made this argument for flat hashes and it holds unchanged at two levels — and a slack of exactly zero is a perfectly ordinary timing result, not a hypothetical.

7. Why This Matters in Engineering Work

Tool reports are statements about named things. "This block, this field, this value" is the natural line format of a timing summary, a DRC report, a utilisation dump — and a hash of hashes is that format held in memory. The three-way split plus one assignment in §2 is the core of a great many working scripts.

Named fields survive edits that positional ones do not. Adding a column to a tool's output breaks every subscript in a positional parser and breaks nothing in this one. For anything you will still be running next year, that alone justifies the shape.

And the exists trap costs real debugging time. A script that checks for optional fields across a set of blocks can silently inflate its own block count, and the symptom — a total that is too high, or a report listing blocks that were never in the input — points nowhere near the exists. Knowing that asking can create is the fastest route to that bug.

8. Common Mistakes

Testing a deep key without guarding. exists $h{a}{b} creates $h{a}. Guard with && from the outside in.

Trusting a top-level key count after optional-field checks. The checks may have added keys.

Forgetting %{ } around a record for keys. keys %{ $r{alu} }, not keys $r{alu}.

Expecting input order from either level. Both are hashes. Sort for output.

Assuming every record has every field. They need not. Use // for a default.

Using || for the default. It replaces a legitimate 0. Use //.

Sorting inside an accumulation loop. Wasted work; sort only where order is observed.

9. Exercise

Exercise 1 — records. Build §1's two-block structure with three fields each. Print the formatted table, the block count, and the field count for one block. Note which of the last two needed %{ }.

Exercise 2 — build from triples. Take §2's five block field value lines, split each into three, and build the structure with one assignment. Print each block's fields joined, sorted. Confirm the output order differs from the input order and say why.

Exercise 3 — uneven records. In Exercise 2's data, confirm one block has three fields and the other two. Then print the field count per block and state what would have to change for Perl to object.

Exercise 4 — the two-level walk. Print every block/field/value triple with both levels sorted. Then list the failing blocks with grep, and total one numeric field without sorting. Say why one of those sorted and the other did not.

Exercise 5 — the trap. Reproduce §4 exactly, including the repeated exists $r{fifo} before and after. Record both answers and the final top-level key count. Write one sentence naming the line that created the key.

Exercise 6 — the guard. Starting from a one-key hash, print the key count, then run an unguarded exists $r{fifo}{slack} and print it again, then reset and run exists $r{regf} && exists $r{regf}{slack} and print it again. Report all three counts.

Exercise 7 — delete. Build a two-block structure where one block has two fields. Delete one inner field and print that block's field count, then delete a whole block and print the top-level count.

Exercise 8 — missing fields. Build two records where only one has a corner field. Print both with // supplying (none), and confirm STDERR is empty. Then remove the // and record what reaches STDERR. Finally, set a field to 0 and show that || and // disagree about it.

10. Summary

  • $report{$block}{$field} is two named lookups — no arrow needed between them.
  • One assignment builds both levels. $report{$blk}{$field} = $value; needs no setup.
  • scalar(keys %report) counts blocks; scalar(keys %{ $report{$blk} }) counts one block's fields.
  • Neither level has an order. Sort both when printing; do not sort when accumulating.
  • Records need not share fields, and nothing warns when they differ.
  • Reaching into level two of a missing level-one key creates it — including from exists. Measured: one top-level key before, two after.
  • Guard with && from the outside in, and the short-circuit prevents the creation. Measured: the key count stays at one.
  • delete works at either level, and deleting a record discards its inner hash.
  • Use // for a missing field's default, never ||, so a legitimate 0 survives.

Chapter 7.9 takes autovivification head on: exactly which positions trigger it, why it is a feature rather than a bug, and how to switch it off where you need a lookup that cannot change anything.

Where this fits

Part of the Perl curriculum.