Skip to content
VLSI Mentor

Perl Part II · Module 26 · Perl with Bash, Tcl, and EDA Tools

Perl Parses What Tcl Produced

This is the division of labour behind most legacy signoff flows: the timing tool's Tcl writes one report per corner, and a Perl stage turns the directory into one table that people and dashboards can use. The usual Perl for it is a five-line glob loop, and it produces the right table whenever the directory is right. This chapter builds six realistic wrong directories — a failed corner, a truncated report, a mislabelled file, a killed run, a stale leftover, a space in the path — and measures what that loop does with each, then writes the collector that checks the hand-off contract before it trusts a single number.

Intermediate24 min readPerlPart IIEDA Flows

Part II · Module 26 · Chapter 26.5 · Perl Parses What Tcl Produced

1. The Input

Chapter 26.4 ran a corner loop for three corners inside a teaching mock of a timing tool. The fast corner, ff_0p88v_m40c, failed because its data was missing; the other two succeeded. This is the directory it left — three files, reproduced exactly as the script wrote them. They are synthetic teaching reports in this module's own format, not the output of any real tool.

Azvya Education Pvt. Ltd.VLSI Mentor
reports/ss_0p72v_125c.rpt
Report : timing (synthetic teaching format)
Design : chip_top
Corner : ss_0p72v_125c

Startpoint: u_core/r_reg[3]
Endpoint  : u_core/acc_reg[7]
  slack (VIOLATED)   -0.043

Startpoint: u_core/cnt_reg[0]
Endpoint  : u_core/cnt_reg[5]
  slack (MET)         0.120

End of report
Azvya Education Pvt. Ltd.VLSI Mentor
reports/tt_0p80v_25c.rpt
Report : timing (synthetic teaching format)
Design : chip_top
Corner : tt_0p80v_25c

Startpoint: u_core/r_reg[3]
Endpoint  : u_core/acc_reg[7]
  slack (MET)         0.212

Startpoint: u_core/cnt_reg[0]
Endpoint  : u_core/cnt_reg[5]
  slack (MET)         0.388

End of report
Azvya Education Pvt. Ltd.VLSI Mentor
reports/manifest.txt
ss_0p72v_125c OK reports/ss_0p72v_125c.rpt
ff_0p88v_m40c FAILED set_corner: no timing data for corner ff_0p88v_m40c (corners/ff_0p88v_m40c.tim)
tt_0p80v_25c OK reports/tt_0p80v_25c.rpt

The job of this chapter's Perl stage is to turn that directory into one table: a CSV with a row per timing path, the corner as a column, so that the worst path across every corner is one sort away. That is the "normalised" output the chapter's purpose describes — the same information as the reports, in one shape, with nothing that depends on which file it came from.

2. The Canonical Loop

Here is the loop almost every flow has some version of. It globs the reports, takes the corner from each filename, parses each report, and prints rows.

Azvya Education Pvt. Ltd.VLSI Mentor
naive_collect.pl
use strict;
use warnings;
use File::Basename qw(basename);

my $dir = shift // "reports";
print "corner,startpoint,endpoint,slack,status\n";
for my $rpt (sort glob "$dir/*.rpt") {
    my ($corner) = basename($rpt) =~ /^(.+)\.rpt$/;
    emit_rows($corner, parse_timing($rpt));
}

sub parse_timing {
    my ($file) = @_;
    open my $fh, "<", $file or die "cannot open $file: $!\n";
    my (@paths, %p);
    while (my $line = <$fh>) {
        if    ($line =~ /^Startpoint:\s*(\S+)/)  { %p = (start => $1) }
        elsif ($line =~ /^Endpoint\s*:\s*(\S+)/) { $p{end} = $1 }
        elsif ($line =~ /^\s+slack \((\w+)\)\s+(-?\d+\.\d+)/) {
            push @paths, { %p, status => $1, slack => $2 };
        }
    }
    close $fh;
    return @paths;
}

sub emit_rows {
    my ($corner, @paths) = @_;
    print join(",", $corner, @{$_}{qw(start end slack status)}), "\n" for @paths;
}
Azvya Education Pvt. Ltd.VLSI Mentor
run_naive.sh
#!/bin/bash
perl naive_collect.pl reports
echo "exit $?"
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
corner,startpoint,endpoint,slack,status
ss_0p72v_125c,u_core/r_reg[3],u_core/acc_reg[7],-0.043,VIOLATED
ss_0p72v_125c,u_core/cnt_reg[0],u_core/cnt_reg[5],0.120,MET
tt_0p80v_25c,u_core/r_reg[3],u_core/acc_reg[7],0.212,MET
tt_0p80v_25c,u_core/cnt_reg[0],u_core/cnt_reg[5],0.388,MET
exit 0

It looks right. Both reports were found, every path is a row, the corner is a column, the violation is visible. Compared by eye with section 1, every value is correct.

What is not in the output is the failed fast corner. The loop asks the directory what exists, and the directory cannot say what should exist. A sign-off reviewer reading this CSV sees two corners, one violation, and no indication that a third corner was requested and never produced.

3. Six Directories, Two Parsers

That is one damaged directory. Real signoff areas get damaged in several distinct ways, and each one is worth testing on purpose rather than discovering in production. The script below builds six copies of the reports directory, each one representing a real failure, and runs both the canonical loop and this chapter's collector (section 5) on every copy.

  • 1 as produced — the directory from section 1: one corner failed inside the tool.
  • 2 truncated — the typical corner's report stops after its first path, as if the disk filled or the tool was killed while writing it directly to its final name.
  • 3 mislabelled — the file named for the typical corner contains the slow corner's report; somebody copied a file to "fill in" a missing corner, or a script wrote to the wrong name.
  • 4 killed mid-run — the job was killed during the typical corner: its report and the manifest exist only under their temporary .tmp names.
  • 5 stale leftover — the run directory was reused, and a report from a different corner in a previous run is still there.
  • 6 space in path — nothing is wrong with the reports at all; the run directory is called run A.

For damage cases 2 to 5, the manifest is the one a run of only the slow and typical corners writes — section 1's manifest without its ff line — so that each case has exactly one thing wrong with it.

Azvya Education Pvt. Ltd.VLSI Mentor
scenarios.pl
use strict;
use warnings;
use File::Path qw(make_path remove_tree);
use File::Copy qw(copy);

# Build damaged copies of the reports directory, each one a real way a
# signoff directory goes wrong, then run both parsers on every copy.
sub slurp { open my $f, "<", $_[0] or die "$_[0]: $!\n"; local $/; <$f> }
sub spew  { open my $f, ">", $_[0] or die "$_[0]: $!\n"; print {$f} $_[1]; close $f }
sub head  { my ($text, $n) = @_; join "", (split /^/, $text)[0 .. $n - 1] }

my $ss  = slurp("reports/ss_0p72v_125c.rpt");
my $tt  = slurp("reports/tt_0p80v_25c.rpt");
my $man = slurp("reports/manifest.txt");
(my $clean_man = $man) =~ s/^ff_\S+ FAILED .*\n//m;   # a run of only ss and tt

my %build = (
    "1 as produced"     => sub { my $d = shift; spew("$d/ss_0p72v_125c.rpt", $ss);
                                 spew("$d/tt_0p80v_25c.rpt", $tt); spew("$d/manifest.txt", $man) },
    "2 truncated"       => sub { my $d = shift; spew("$d/ss_0p72v_125c.rpt", $ss);
                                 spew("$d/tt_0p80v_25c.rpt", head($tt, 9)); spew("$d/manifest.txt", $clean_man) },
    "3 mislabelled"     => sub { my $d = shift; spew("$d/ss_0p72v_125c.rpt", $ss);
                                 spew("$d/tt_0p80v_25c.rpt", $ss); spew("$d/manifest.txt", $clean_man) },
    "4 killed mid-run"  => sub { my $d = shift; spew("$d/ss_0p72v_125c.rpt", $ss);
                                 spew("$d/tt_0p80v_25c.rpt.tmp", head($tt, 9));
                                 spew("$d/manifest.txt.tmp", head($clean_man, 1)) },
    "5 stale leftover"  => sub { my $d = shift; spew("$d/ss_0p72v_125c.rpt", $ss);
                                 spew("$d/tt_0p80v_25c.rpt", $tt); spew("$d/manifest.txt", $clean_man);
                                 (my $old = $ss) =~ s/ss_0p72v_125c/ss_0p72v_m40c/;
                                 spew("$d/ss_0p72v_m40c.rpt", $old) },
    "6 space in path"   => sub { my $d = shift; spew("$d/ss_0p72v_125c.rpt", $ss);
                                 spew("$d/tt_0p80v_25c.rpt", $tt); spew("$d/manifest.txt", $clean_man) },
);

for my $name (sort keys %build) {
    (my $case = "case_$name") =~ s/ /_/g;
    my $dir = $name =~ /space/ ? "$case/run A/reports" : "$case/reports";
    make_path($dir, "$case/out");
    $build{$name}->($dir);
    print "$name\n";

    my ($csv, $nerr, $nrc) = run3("perl", "naive_collect.pl", $dir);
    my @rows = grep { !/^corner,/ } split /\n/, $csv;
    my %corners = map { (split /,/)[0] => 1 } @rows;
    my ($tt_worst) = sort { $a <=> $b } map { (split /,/)[3] } grep { /^tt_/ } @rows;
    printf "  %-19s exit %d  %s\n", "naive_collect.pl", $nrc, $nrc
        ? "died: " . (split /\n/, $nerr)[0]
        : sprintf("corners=%d paths=%d tt_worst=%s",
                  scalar(keys %corners), scalar(@rows), $tt_worst // "(none)");

    my (undef, $err, $rc) = run3("perl", "collect_corners.pl", $dir, "$case/out");
    my @bad = grep { !/^\S+\s+OK\b/ } split /\n/, $err;
    s/\s+/ /g for @bad;
    printf "  %-19s exit %d  %s\n", "collect_corners.pl", $rc, @bad ? shift @bad : "every corner OK" . ($rc == 1 ? ", violations present" : "");
    printf "  %-19s         %s\n", "", $_ for @bad;
}

# Run a command; return its stdout, its stderr and its exit code.
sub run3 {
    my @cmd = @_;
    my $pid = open my $fh, "-|" // die "fork: $!\n";
    if (!$pid) { open STDERR, ">", "stderr.tmp" or die; exec @cmd or exit 127 }
    my $out = do { local $/; <$fh> } // "";
    close $fh;
    my $rc = $? >> 8;
    my $err = -e "stderr.tmp" ? slurp("stderr.tmp") : "";
    unlink "stderr.tmp";
    return ($out, $err, $rc);
}
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
1 as produced
  naive_collect.pl    exit 0  corners=2 paths=4 tt_worst=0.212
  collect_corners.pl  exit 2  ff_0p88v_m40c FAILED_IN_TOOL set_corner: no timing data for corner ff_0p88v_m40c (corners/ff_0p88v_m40c.tim)
2 truncated
  naive_collect.pl    exit 0  corners=2 paths=3 tt_worst=0.212
  collect_corners.pl  exit 2  tt_0p80v_25c INCOMPLETE no 'End of report' line
3 mislabelled
  naive_collect.pl    exit 0  corners=2 paths=4 tt_worst=-0.043
  collect_corners.pl  exit 2  tt_0p80v_25c MISLABELLED file is named tt_0p80v_25c but reports corner ss_0p72v_125c
4 killed mid-run
  naive_collect.pl    exit 0  corners=1 paths=2 tt_worst=(none)
  collect_corners.pl  exit 2  problem: no manifest.txt -- the tool run did not finish
                              problem: leftover manifest.txt.tmp -- a file was being written when the run stopped
                              problem: leftover tt_0p80v_25c.rpt.tmp -- a file was being written when the run stopped
5 stale leftover
  naive_collect.pl    exit 0  corners=3 paths=6 tt_worst=0.212
  collect_corners.pl  exit 2  ss_0p72v_m40c NOT_REQUESTED not in the manifest
6 space in path
  naive_collect.pl    exit 2  died: cannot open case_6_space_in_path/run: No such file or directory
  collect_corners.pl  exit 1  every corner OK, violations present

The canonical loop exited 0 on five of the six directories. On the sixth it died — and that is the case where nothing was wrong.

Read case 3 closely, because it is the worst one. The loop reported the typical corner's worst slack as -0.043. The real typical corner has no violations at all; -0.043 is the slow corner's number, read from a file that carries the typical corner's name. A reviewer would see a violation at a corner where none exists — or, in the opposite direction, a clean result copied into a corner that actually fails.

The collector flagged all five damaged directories, each with a status naming what was wrong and where, and exited 2: the contract's "signoff data incomplete". On the sixth it parsed both reports correctly and exited 1, because the slow corner really does violate.

4. Why Each One Fooled the Loop

Each failure of the loop has one specific cause, and each cause is a wrong assumption about where the truth lives.

Case 1, a failed corner — "the directory says what exists". It does, and that is all it says. A corner that failed in the tool leaves no file, and an absent file looks identical to a corner nobody asked for. Only the manifest knows what was requested, which is why Chapter 26.4 wrote one. The loop never reads it.

Case 2, a truncated report — "if the file opens, it is complete". The loop parses whatever paths it finds and stops at end-of-file. A report cut short after one path is indistinguishable from a report that only had one path — unless the parser checks for the end marker. That is Chapter 21.2's argument about a crashed simulation log, applied to a timing report: the evidence of an unfinished file is a line that is missing, and only a parser that looks for it will notice.

Case 3, a mislabelled report — "the filename is the corner". The filename is a claim about the corner, made by whatever wrote the file. The Corner : line inside is the tool's own statement of what it was analysing. When they disagree, at least one of them is wrong, and a parser that only reads the filename has no way to find out. Checking one against the other costs one regular expression.

Case 4, a killed run — "everything in the directory is finished". The loop's *.rpt glob correctly ignores tt_0p80v_25c.rpt.tmp, so the half-written report does not leak into the table. But ignoring it silently is the wrong half of the right idea: a .tmp file is evidence that the run stopped while writing, and the missing manifest is evidence that the corner loop never reached its end. The loop reports one clean corner and exits 0.

Case 5, a stale leftover — "everything in the directory belongs to this run". A reused run directory accumulates files. The loop included a report from a different corner and a different run, and nothing about the output marks it as stale. The manifest is again the answer: a report that is not in it was not produced by this run.

Case 6, a space in the path — "glob takes a path". It does not. Perl's built-in glob follows the C shell's rules, and one of those rules is that whitespace separates patterns. glob "run A/reports/*.rpt" is two patterns — run and A/reports/*.rpt — and since a pattern with no wildcard is returned as-is, the first result is the literal string run. The loop then tries to open a file called run. File::Glob's bsd_glob takes exactly one pattern, spaces included, and is the right function whenever the directory comes from somewhere you do not control.

Azvya Education Pvt. Ltd.VLSI Mentor
glob_space.pl
use strict;
use warnings;
use File::Path qw(make_path);
use File::Glob qw(bsd_glob);

make_path("run A/reports");
for my $c (qw(ss tt)) { open my $f, ">", "run A/reports/$c.rpt" or die; close $f }

my $dir = "run A/reports";
my @g = glob "$dir/*.rpt";
my @b = bsd_glob("$dir/*.rpt");
printf "glob     -> %d result(s): %s\n", scalar @g, join(" | ", @g);
printf "bsd_glob -> %d result(s): %s\n", scalar @b, join(" | ", @b);
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
glob     -> 1 result(s): run
bsd_glob -> 2 result(s): run A/reports/ss.rpt | run A/reports/tt.rpt

5. The Collector

The collector reverses the loop's order of trust. It starts from what was requested (the manifest), compares it with what exists (the directory), gives every corner exactly one status, and only then parses the reports it has decided to trust.

Azvya Education Pvt. Ltd.VLSI Mentor
collect_corners.pl
#!/usr/bin/perl
# collect_corners.pl -- turn a directory of per-corner timing reports into one
# CSV of paths and one CSV of corner status, checking the 26.4 contract.
#
# Exit: 0  every requested corner complete, no violations
#       1  every requested corner complete, violations found
#       2  signoff data incomplete: a corner failed, is missing, truncated or
#          mislabelled, or the tool run did not finish
use strict;
use warnings;
use File::Glob qw(bsd_glob);
use File::Basename qw(basename);
use File::Temp qw(tempfile);

my $dir = shift // "reports";
my $out = shift // ".";
my @problems;

# 1. The manifest says what was requested. No manifest: the run never finished.
my %manifest;
if (open my $fh, "<", "$dir/manifest.txt") {
    while (my $line = <$fh>) {
        chomp $line;
        my ($corner, $state, $detail) = split / /, $line, 3;
        $manifest{$corner} = { state => $state, detail => $detail // "" };
    }
    close $fh;
} else {
    push @problems, "no manifest.txt -- the tool run did not finish";
}

# 2. What is really in the directory. bsd_glob treats a space in $dir as data.
my %file_for;
for my $path (bsd_glob("$dir/*.rpt")) {
    my ($corner) = basename($path) =~ /^(.+)\.rpt\z/;
    $file_for{$corner} = $path;
}
push @problems, "leftover " . basename($_) . " -- a file was being written when the run stopped"
    for bsd_glob("$dir/*.tmp");

# 3. Every corner that was requested OR found gets exactly one status.
my (@rows, @status);
my %seen = (%manifest, %file_for);
for my $corner (sort keys %seen) {
    my $m = $manifest{$corner};
    my %s = (corner => $corner, paths => "", viol => "", worst => "", detail => "");
    if ($m && $m->{state} eq "FAILED") {
        @s{qw(status detail)} = ("FAILED_IN_TOOL", $m->{detail});
    } elsif (!$file_for{$corner}) {
        @s{qw(status detail)} = ("NO_REPORT", "manifest says OK but the file is missing");
    } else {
        my $r = parse_timing($file_for{$corner});
        if (!$r->{complete}) {
            @s{qw(status detail)} = ("INCOMPLETE", "no 'End of report' line");
        } elsif (($r->{corner} // "") ne $corner) {
            @s{qw(status detail)} = ("MISLABELLED",
                "file is named $corner but reports corner " . ($r->{corner} // "(none)"));
        } elsif (%manifest && !$m) {
            @s{qw(status detail)} = ("NOT_REQUESTED", "not in the manifest");
        } else {
            my @p = @{ $r->{paths} };
            my @v = grep { $_->{slack} < 0 } @p;
            my ($worst) = sort { $a <=> $b } map { $_->{slack} } @p;
            @s{qw(status paths viol worst)} = ("OK", scalar @p, scalar @v, $worst // "");
            push @rows, map { [ $corner, @{$_}{qw(start end slack status)} ] } @p;
        }
    }
    push @status, \%s;
}

write_csv("$out/paths.csv",   [qw(corner startpoint endpoint slack status)], \@rows);
write_csv("$out/corners.csv", [qw(corner status paths violations worst detail)],
          [ map { [ @{$_}{qw(corner status paths viol worst detail)} ] } @status ]);

# 4. A summary for people, on stderr; the CSVs are the data.
printf STDERR "%-15s %-15s %s\n", $_->{corner}, $_->{status},
    $_->{status} eq "OK" ? "paths=$_->{paths} violations=$_->{viol} worst=$_->{worst}"
                         : $_->{detail}
    for @status;
print STDERR "problem: $_\n" for @problems;

my $incomplete = @problems || grep { $_->{status} ne "OK" } @status;
exit 2 if $incomplete;
exit((grep { $_->{viol} } @status) ? 1 : 0);

sub parse_timing {
    my ($file) = @_;
    my %r = (paths => [], complete => 0);
    open my $fh, "<", $file or return \%r;
    my %p;
    while (my $line = <$fh>) {
        if    ($line =~ /^Corner\s*:\s*(\S+)/)    { $r{corner} = $1 }
        elsif ($line =~ /^Startpoint:\s*(\S+)/)   { %p = (start => $1) }
        elsif ($line =~ /^Endpoint\s*:\s*(\S+)/)  { $p{end} = $1 }
        elsif ($line =~ /^\s+slack \((MET|VIOLATED)\)\s+(-?\d+\.\d+)\s*$/) {
            push @{ $r{paths} }, { %p, status => $1, slack => $2 };
        }
        elsif ($line =~ /^End of report$/)        { $r{complete} = 1 }
    }
    close $fh;
    return \%r;
}

sub csv_field {
    my ($v) = @_;
    return "" unless defined $v;
    return $v unless $v =~ /[",\r\n]/;
    $v =~ s/"/""/g;
    return qq{"$v"};
}

sub write_csv {
    my ($path, $cols, $rows) = @_;
    my ($fh, $tmp) = tempfile("$path.XXXXXX");
    print {$fh} join(",", @$cols), "\n";
    print {$fh} join(",", map { csv_field($_) } @$_), "\n" for @$rows;
    close $fh or die "cannot finish $tmp: $!\n";
    rename $tmp, $path or die "cannot rename $tmp to $path: $!\n";
}

Read it as four steps, numbered in the comments.

Step 1 reads the manifest first. If there is no manifest, the tool's corner loop never finished (Chapter 26.4 renames it into place only at the end), and that is recorded as a problem before a single report is opened. Each manifest line is split into at most three fields, so a failure message containing spaces stays whole.

Step 2 lists the directory with bsd_glob and records every leftover .tmp file as a problem. A file being written when the run stopped is a fact about the run, not noise to be filtered.

Step 3 gives every corner exactly one status. The set of corners is the union of the manifest and the directory, so a corner that is only in the manifest (it failed, or its file went missing) and a corner that is only in the directory (a stale leftover) both get a row. The checks run in a fixed order, and each later check is only meaningful because the earlier ones passed:

StatusMeaningChecked
FAILED_IN_TOOLthe manifest says the tool failed for this cornerfirst — the reason comes from the tool
NO_REPORTthe manifest says OK, the file is missingbefore any parsing
INCOMPLETEthe report has no end markerbefore trusting any value in it
MISLABELLEDthe filename and the Corner : line disagreeafter completeness — a truncated file may have no header
NOT_REQUESTEDa report the manifest does not listafter the file is known to be a real report
OKcomplete, correctly labelled, requestedonly by elimination

OK is last and reached only when nothing else applied — the same structure as Chapter 21.8's verdict ladder, and for the same reason: written as a condition, it would be satisfied by a report that simply had no problems the author thought of.

Step 4 writes two CSVs and an exit status. paths.csv is the normalised table — only from corners whose status is OK, so a truncated or mislabelled report contributes no rows at all. corners.csv is the status table, one row per corner including the ones with no data, so the gap is in the output rather than only in a log message. Both are written through a temporary file and a rename (Chapter 13.13), and quoted with the CSV rule from Chapter 26.3. The summary for people goes to stderr; the exit status follows Chapter 26.1's contract.

Figure 26.5.1 — The collector's order of trust

Six stages: read the manifest of requested corners; list the directory with bsd_glob, noting leftover temporary files; classify every corner in the union of the two with exactly one status; parse only the corners classified OK; write the paths table and the corner status table atomically; exit 0, 1 or 2 by the contract.collect_corners.pl1Read the manifestwhat was requested2List the directorybsd_glob, and every .tmp file3Classify each cornerone status, before any parsing4Parse the OK cornerscomplete, labelled, requested5Write two tablespaths.csv and corners.csv6Exit by contract0 clean, 1 violations, 2 incomplete
The manifest comes first because it is the only statement of what should exist. The directory listing comes second and includes the leftovers a careless glob would hide. Classification gives every corner exactly one status before any number is read, so a report is parsed only once it is known to be complete, correctly labelled and requested. Only then is anything written, and the exit status reports whether the signoff data is complete, not merely whether the script ran.

6. Running It, and What "Normalised" Means

On the directory exactly as the tool produced it:

Azvya Education Pvt. Ltd.VLSI Mentor
run_collect.sh
#!/bin/bash
mkdir -p out
echo "--- the summary, from stderr"
perl collect_corners.pl reports out 2>&1
echo "exit $?"
echo "--- out/paths.csv"
cat out/paths.csv
echo "--- out/corners.csv"
cat out/corners.csv
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
--- the summary, from stderr
ff_0p88v_m40c   FAILED_IN_TOOL  set_corner: no timing data for corner ff_0p88v_m40c (corners/ff_0p88v_m40c.tim)
ss_0p72v_125c   OK              paths=2 violations=1 worst=-0.043
tt_0p80v_25c    OK              paths=2 violations=0 worst=0.212
exit 2
--- out/paths.csv
corner,startpoint,endpoint,slack,status
ss_0p72v_125c,u_core/r_reg[3],u_core/acc_reg[7],-0.043,VIOLATED
ss_0p72v_125c,u_core/cnt_reg[0],u_core/cnt_reg[5],0.120,MET
tt_0p80v_25c,u_core/r_reg[3],u_core/acc_reg[7],0.212,MET
tt_0p80v_25c,u_core/cnt_reg[0],u_core/cnt_reg[5],0.388,MET
--- out/corners.csv
corner,status,paths,violations,worst,detail
ff_0p88v_m40c,FAILED_IN_TOOL,,,,set_corner: no timing data for corner ff_0p88v_m40c (corners/ff_0p88v_m40c.tim)
ss_0p72v_125c,OK,2,1,-0.043,
tt_0p80v_25c,OK,2,0,0.212,

The paths table is identical to the naive loop's output in section 2 — on a good report, both parsers read the same numbers. The difference is everything around it: the failed fast corner is a row in corners.csv with the tool's own reason, the empty paths, violations and worst fields say "no data" rather than claiming zero (Chapter 26.3's rule), and the exit status is 2 because a signoff with a missing corner is incomplete.

"Normalised" deserves a precise meaning, because it is easy to claim:

  • One row per path, the corner as a column. Nothing about a row depends on which file it came from.
  • One spelling per value. MET and VIOLATED are copied from a fixed set the parser checks for; a report with any other status word is not parsed as a path.
  • The slack exactly as the tool wrote it. 0.120, not 0.12. CSV has no types, so there is no reason to re-format a number and a good reason not to: a reviewer comparing the table to the report should see the same characters.
  • A deterministic order. Corners are sorted by name, and within a corner the paths stay in report order, which in this format is worst first (Chapter 17.8).
  • The status of every corner, alongside the data. A normalised table that silently omits a corner is not normalised; it is incomplete.

One thing this synthetic format leaves out on purpose is units. A real timing report states its time unit in its header, and different libraries or tool settings can produce different units. A real collector must read the unit from each report, record it, and refuse to put two different units into one column — another header check, exactly like the Corner : check above.

7. Exercises

1. Add a seventh scenario. Write a damaged directory in which the manifest says OK for the typical corner but the report file is empty (zero bytes). Predict both parsers' output, add it to scenarios.pl, and run it. Which status does the collector give it, and is that the most useful one? Chapter 18.9 distinguishes empty from missing; should the collector?

2. Mislabelled the other way. In case 3 the slow corner's violation was copied into the typical corner. Build the reverse: the typical corner's clean report copied over the slow corner's file. What does the naive loop report as the worst slack in the design, and why is this direction more dangerous for a tape-out decision than case 3?

3. Units. Extend the synthetic format with a Units : ns header line and change the collector so that a report with no units line is INCOMPLETE and a mix of ns and ps across corners stops the run with exit 2. Write the scenario that proves the second check can fail.

4. Worst path per corner. Add a third output, worst.csv, with one row per OK corner holding its worst path. The file must be identical every run. What should it contain for a corner whose status is not OK — no row, or a row with empty fields? Justify the choice with Chapter 26.3's rule about empty and zero.

5. Make the loop survive review. Without rewriting it into the collector, make the smallest set of changes to naive_collect.pl that would have caught cases 2, 3 and 6. Then explain why no change to the loop alone can catch case 1 — and what that says about where the fix for case 1 belongs.

8. What To Remember

  • The Tcl writes the reports; this Perl stage turns a directory of them into one table.
  • The canonical glob loop is right on a good directory — and that is the only kind it has usually seen.
  • Measured: it exited 0 on 5 of 6 damaged directories, and died on the only undamaged one.
  • It reported -0.043 as the typical corner's worst slack — the slow corner's number from a mislabelled file.
  • A directory lists what exists, not what was requested. Read the manifest first.
  • A truncated report looks like a short one. Require the end marker before trusting any value (21.2).
  • A filename is a claim; the Corner : line is evidence. Check one against the other.
  • A .tmp file is evidence the run stopped while writing, not noise to filter out.
  • A report not in the manifest is stale. Reused run directories accumulate them.
  • Built-in glob splits on whitespace: run A/reports/*.rpt returned run. Use bsd_glob.
  • Give every corner in the union of manifest and directory exactly one status, with OK only by elimination.
  • Write the data table and the status table, atomically, and exit 0, 1 or 2 by the contract (26.1).
  • Normalised means one row per path, one spelling per value, the tool's exact text, a fixed order, and every corner accounted for.

Chapter 26.6 turns the hand-off around. Instead of reading what a Tcl script produced, Perl writes the Tcl script — and the values it substitutes into that script have to survive Tcl's own quoting rules, which are different from every language covered so far.

Where this fits

Part of the Perl curriculum.