Perl Part II · Module 29 · Practical VLSI Automation Projects
Project: Per-Corner Timing Report Parser
Module 23 took a timing report apart field by field: where the corner is, where the verdict lives in each tool's layout, why the sign of a slack is not the verdict, why int() is the wrong floor, and why a tie at the cut changes between processes. This project puts all of it into one tool that a nightly flow can gate on. It reads a directory of per-corner reports in either layout, refuses anything it cannot read completely, writes one normalised CSV, and prints the per-corner summary, the worst paths and a histogram — with an exit status that says whether anything failed.
Advanced22 min readPerlPart IIProjectsStatic Timing
Part II · Module 29 · Chapter 29.3 · Project: Per-Corner Timing Report Parser
1. The Brief
| Command | parse_sta.pl REPORT_DIR [--top N] [--csv FILE] [--bin NS] |
| Reads | every *.rpt in the directory; each file is one corner, in PrimeTime or Tempus layout |
| Writes | --csv: one row per path, corner,endpoint,startpoint,group,type,slack,verdict. STDOUT: per-corner summary, the worst N paths, a slack histogram |
| Exit 0 | every path met |
| Exit 1 | at least one path is VIOLATED |
| Exit 2 | an input problem: an unrecognised or cut-off report, a corner in two files, a corner with no paths, an empty directory — nothing is written |
| Exit 3 | usage error |
Exit 2 outranks exit 1. If one corner cannot be read, the run does not know whether everything that matters met timing, and "1 — something failed" would let a reader assume the rest was checked. Chapter 21.8 made the same argument for test verdicts: the order of the rules is the rule, and the rule that says "the evidence is incomplete" comes first.
The project applies Module 23 and does not re-derive it. The chapters it leans on most:
- Detecting violated endpoints — the verdict comes from the annotation, never the sign (23.3).
- Tempus report formats — the verdict lives in a different place in each layout (23.7).
- Normalising reports before downstream tools — detect the format first, and refuse an unknown one (23.8).
- Top-N endpoints and slack histograms — clamp the slice,
floor()notint(), break ties (23.10).
2. The Sample Reports
No static timing tool exists on this machine, and this track never publishes invented tool output. So make_reports.pl writes sample reports in the two layouts Chapters 23.6 and 23.7 described, from a small, labelled arithmetic model of a four-lane adder: every path launches from a flop, ripples through one carry stage per bit, and its slack is the required time minus the arrival time, scaled by a per-corner derate.
The important property is not realism of the numbers. It is that the generator knows the truth: it writes every path's corner, endpoint, startpoint, type, printed slack and verdict to expected.tsv. The parser's correctness can then be measured as a round trip — does it recover exactly what was written?
#!/usr/bin/perl
# make_reports.pl -- write SAMPLE per-corner timing reports for this chapter.
# Usage: perl make_reports.pl DIR
#
# No STA tool exists on this machine. These files are generated in the two
# layouts Chapters 23.6 and 23.7 described -- a PrimeTime-style report for
# two corners and a Tempus-style report for the third -- from a small
# arithmetic model of a 4-lane, 8-bit adder block. The model is labelled
# and simple on purpose: what this chapter measures is whether the PARSER
# recovers exactly what was written, and the generator also writes that
# truth to DIR/expected.tsv so the round trip can be checked.
use strict;
use warnings;
use File::Path qw(make_path);
my $dir = shift // die "usage: perl make_reports.pl DIR\n";
make_path("$dir/reports");
my %CORNER = ( # derate on cell delays; period 1.0 ns, setup and uncertainty fixed
typ_1v00_25c => { derate => 1.00, format => "pt" },
slow_0v90_125c => { derate => 1.37, format => "tempus" },
fast_1v10_m40c => { derate => 0.82, format => "pt" },
);
my ($PERIOD, $SETUP, $UNCERT, $CLK2Q, $STAGE) = (1.0, 0.065, 0.030, 0.150, 0.082);
# one path per endpoint: lane L, bit B launches from a_reg_0_ and ripples
# through B carry stages. Lanes 0 and 1 are identical, so their slacks TIE.
sub paths_for {
my ($corner) = @_;
my $d = $CORNER{$corner}{derate};
my @p;
for my $lane (0 .. 3) {
for my $bit (0 .. 7) {
my $extra = $lane < 2 ? 0 : 0.004 * ($lane - 1);
my @stages = ([ "lane$lane/a_reg_0_/Q (DFCNQD1)", $CLK2Q * $d ]);
push @stages, [ "lane$lane/U${_}1/Z (XOR2D1)", ($STAGE + $extra) * $d ] for 0 .. $bit;
my $arrival = 0;
$arrival += $_->[1] for @stages;
my $required = $PERIOD - $SETUP - $UNCERT;
push @p, { start => "lane$lane/a_reg_0_/Q", end => "lane$lane/sum_reg_${bit}_/D",
stages => \@stages, arrival => $arrival, required => $required,
slack => $required - $arrival };
}
}
# two paths that fail by less than the report's last printed digit, so
# the report prints -0.0000: a setup check at the slow corner, and a hold
# check (arrival must EXCEED the hold requirement) at the fast corner
push @p, { type => "max", start => "ctl/cnt_reg_3_/Q", end => "ctl/done_reg/D",
stages => [ [ "ctl/cnt_reg_3_/Q (DFCNQD1)", 0.40 ], [ "ctl/U9/Z (AN2D1)", 0.50504 ] ],
arrival => 0.90504, required => $PERIOD - $SETUP - $UNCERT,
slack => $PERIOD - $SETUP - $UNCERT - 0.90504 } if $corner eq "slow_0v90_125c";
push @p, { type => "min", start => "ctl/sync_reg_0_/Q", end => "ctl/sync_reg_1_/D",
stages => [ [ "ctl/sync_reg_0_/Q (DFCNQD1)", 0.02497 ] ],
arrival => 0.02497, required => 0.025, slack => 0.02497 - 0.025 }
if $corner eq "fast_1v10_m40c";
$_->{type} //= "max" for @p;
return sort { $a->{slack} <=> $b->{slack} or $a->{end} cmp $b->{end} } @p;
}
open my $truth, ">", "$dir/expected.tsv" or die $!;
for my $corner (sort keys %CORNER) {
my @paths = paths_for($corner);
my $file = "$dir/reports/$corner.$CORNER{$corner}{format}.rpt";
open my $fh, ">", $file or die "$file: $!";
print {$fh} $CORNER{$corner}{format} eq "pt" ? pt_header($corner) : tempus_header();
my $n = 0;
for my $p (@paths) {
my $verdict = $p->{slack} < 0 ? "VIOLATED" : "MET"; # decided BEFORE rounding
my $shown = sprintf "%.4f", $p->{slack};
print {$fh} $CORNER{$corner}{format} eq "pt"
? pt_block($p, $verdict, $shown) : tempus_block(++$n, $corner, $p, $verdict, $shown);
print {$truth} join("\t", $corner, $p->{end}, $p->{start}, $p->{type}, $shown, $verdict), "\n";
}
close $fh;
}
close $truth;
sub pt_header {
my ($corner) = @_;
return "****************************************\nReport : timing\n"
. " -path_type full\n -delay_type max\n -max_paths 50\n"
. "Design : adder_block\nCorner : $corner\n"
. "Generated by make_reports.pl (a sample in the PrimeTime layout)\n"
. "****************************************\n\n";
}
sub pt_block {
my ($p, $verdict, $shown) = @_;
my $s = " Startpoint: $p->{start}\n"
. " (rising edge-triggered flip-flop clocked by clk)\n"
. " Endpoint: $p->{end}\n"
. " (rising edge-triggered flip-flop clocked by clk)\n"
. " Path Group: clk\n Path Type: $p->{type}\n\n"
. sprintf(" %-40s %8s %8s\n", "Point", "Incr", "Path")
. " " . ("-" x 60) . "\n"
. sprintf(" %-40s %8.4f %8.4f\n", "clock clk (rise edge)", 0, 0);
my $t = 0;
for my $st (@{ $p->{stages} }) {
$t += $st->[1];
$s .= sprintf " %-40s %8.4f %8.4f r\n", $st->[0], $st->[1], $t;
}
$s .= sprintf(" %-40s %8.4f %8.4f\n\n", "data arrival time", 0, $p->{arrival});
$s .= $p->{type} eq "max" # setup: required - arrival; hold: arrival - required
? sprintf(" %-40s %8s %8.4f\n", "data required time", "", $p->{required})
. sprintf(" %-40s %8s %8.4f\n", "data arrival time", "", -$p->{arrival})
: sprintf(" %-40s %8s %8.4f\n", "data arrival time", "", $p->{arrival})
. sprintf(" %-40s %8s %8.4f\n", "data required time", "", -$p->{required});
$s .= " " . ("-" x 60) . "\n"
. sprintf(" %-40s %8s %8s\n\n", "slack ($verdict)", "", $shown);
return $s;
}
sub tempus_header {
return "#" x 60 . "\n# Generated by make_reports.pl (a sample in the Tempus layout)\n"
. "#" x 60 . "\n\n";
}
sub tempus_block {
my ($n, $corner, $p, $verdict, $shown) = @_;
return "Path $n: $verdict Setup Check with Pin $p->{end}\n"
. sprintf("Endpoint: %-30s (^) checked with leading edge of 'clk'\n", $p->{end})
. sprintf("Beginpoint: %-30s (^) triggered by leading edge of 'clk'\n", $p->{start})
. "Path Groups: {clk}\nAnalysis View: $corner\n"
. sprintf("%-31s %.3f\n", "- Setup", $SETUP)
. sprintf("%-31s %.3f\n", "+ Phase Shift", $PERIOD)
. sprintf("%-31s %.3f\n", "- Uncertainty", $UNCERT)
. sprintf("%-31s %.3f\n", "= Required Time", $p->{required})
. sprintf("%-31s %.4f\n", "- Arrival Time", $p->{arrival})
. sprintf("%-31s %s\n\n", "= Slack Time", $shown);
}Two details of the model are there to test the parser, and both come from Module 23:
- Lanes 0 and 1 are identical, so their slacks tie exactly. A ranking without a tie-break is free to list them in either order (Chapter 23.10).
- Two paths fail by less than the last printed digit: a setup check in the slow corner (Tempus layout) and a hold check in the fast corner (PrimeTime layout). The tool decides the verdict on the unrounded value and then prints the rounded one, so the report says
-0.0000andVIOLATED. Real reports do this; Chapter 23.3 measured that a sign test calls it met.
3. What A Printed Slack Turns Into
Before the design, one measurement that shapes it. A slack is printed text. What happens to it when a parser treats it as a number?
#!/bin/bash
# negzero.sh -- what happens to a printed slack once it becomes a number.
perl -e '
use strict; use warnings; use POSIX qw(floor);
printf "%-8s %-9s %-14s %-10s %-6s %s\n",
"TEXT", "AS NUMBER", "AFTER +0 %.4f", "TEXT %.4f", "< 0 ?", "BUCKET";
for my $text ("-0.0074", "-0.0000", "0.0000") {
my $num = $text + 0;
printf "%-8s %-9s %-14s %-10s %-6s %s\n", $text, $num,
sprintf("%.4f", $num), sprintf("%.4f", $text),
($text < 0 ? "yes" : "no"), floor($text / 0.05);
}
'TEXT AS NUMBER AFTER +0 %.4f TEXT %.4f < 0 ? BUCKET
-0.0074 -0.0074 -0.0074 -0.0074 yes -1
-0.0000 0 0.0000 -0.0000 no 0
0.0000 0 0.0000 0.0000 no 0-0.0074 survives anything. -0.0000 does not. As text it is negative; the moment it takes part in arithmetic, the result is plain 0, and formatting that number prints 0.0000. Formatting the original string still prints the minus sign — so whether the sign survives depends on whether some earlier line of the program happened to do arithmetic with the value. It is not less than zero, so a sign test calls it met, and floor puts it in the bucket that starts at zero — the passing side of the histogram.
Three design rules follow, and the tool follows all three:
- The verdict always comes from the annotation,
(VIOLATED)in PrimeTime and thePath N:header in Tempus. - The CSV keeps the slack text exactly as the report printed it. Normalising the layout is the job; reformatting the numbers is not.
- The histogram counts violated and met paths separately in each bucket, so a violated path in the zero bucket is visible as a
V.
4. The Design
Figure 29.3.1 — Read everything, refuse anything partial, then rank
Detection comes before reading, and it must be unambiguous. Chapter 23.7 measured what happens when one tool's parser reads the other tool's report: it finds a number with no annotation where it expected one. Each format has a recognition pattern, and a file that matches none — or, after some future edit, both — is a problem rather than a guess.
The corner comes from the right place in each layout. PrimeTime prints it once in the header, which is why Chapter 23.1 found that no path block carries it. The Tempus layout prints an Analysis View: line in every block, so the reader collects them and refuses a file that names more than one.
A cut-off report is a problem, not a shorter report. Chapter 23.9 measured that a missing value can turn a violation into a pass: drop the one corner where an endpoint fails and its worst case looks comfortable. So a path block that lacks any of its six fields stops the run, and the message names the file, the path's position and the fields that are missing.
5. The Tool
#!/usr/bin/perl
# parse_sta.pl -- read a directory of per-corner timing reports (PrimeTime
# or Tempus layout), write one normalised CSV, and summarise: per-corner
# WNS/TNS, the top-N worst endpoints and a slack histogram.
use strict;
use warnings;
use Getopt::Long qw(GetOptions);
use POSIX qw(floor);
use constant { ALL_MET => 0, VIOLATED => 1, INPUT => 2, USAGE => 3 };
my %opt = (top => 20, bin => 0.05);
GetOptions(\%opt, "top=i", "csv=s", "bin=f", "help") or usage("bad option");
usage() if $opt{help};
usage("need exactly one report directory") unless @ARGV == 1;
usage("--top must be at least 1") if $opt{top} < 1;
usage("--bin must be positive") if $opt{bin} <= 0;
my $dir = $ARGV[0];
fail(INPUT, "$dir is not a directory") unless -d $dir;
# ---- each format: how to recognise it, and how to read one file of it
my %FORMAT = (
primetime => { looks => qr/^Report : timing\b/m, read => \&read_primetime },
tempus => { looks => qr/^Path \d+: (?:MET|VIOLATED) /m, read => \&read_tempus },
);
# ---- read every report; every problem is collected, none is skipped
my (@paths, %corner_from, @problems);
my @files = sort glob("$dir/*.rpt");
push @problems, "no *.rpt files in $dir" unless @files;
for my $file (@files) {
my $text = do { local $/; open my $fh, "<", $file or fail(INPUT, "$file: $!"); <$fh> };
my @fmt = grep { $text =~ $FORMAT{$_}{looks} } sort keys %FORMAT;
if (@fmt != 1) {
push @problems, "$file: " . (@fmt ? "matches more than one format" : "not a recognised timing report");
next;
}
my ($corner, @got) = $FORMAT{ $fmt[0] }{read}->($file, $text);
next unless defined $corner;
if (my $prev = $corner_from{$corner}) {
push @problems, "$file: corner $corner was already read from $prev";
next;
}
$corner_from{$corner} = $file;
push @problems, "$file: no paths in corner $corner" unless @got;
push @paths, @got;
}
if (@problems) {
print STDERR "parse_sta.pl: $_\n" for @problems;
fail(INPUT, scalar(@problems) . " input problem(s); nothing written");
}
# ---- the normalised CSV: one row per path, fixed columns, fixed order
if ($opt{csv}) {
my $tmp = "$opt{csv}.tmp.$$";
open my $out, ">", $tmp or fail(USAGE, "cannot write $tmp: $!");
print {$out} "corner,endpoint,startpoint,group,type,slack,verdict\n";
for my $p (sort { $a->{corner} cmp $b->{corner} or $a->{end} cmp $b->{end} } @paths) {
print {$out} join(",", @{$p}{qw(corner end start group type slack verdict)}), "\n";
}
close $out or fail(USAGE, "cannot write $tmp: $!");
rename $tmp, $opt{csv} or fail(USAGE, "cannot rename $tmp: $!");
}
# ---- per corner: paths, violations, worst slack, total negative slack
printf "%-16s %6s %9s %9s %9s %s\n", "CORNER", "PATHS", "VIOLATED", "WNS", "TNS", "FORMAT";
for my $c (sort keys %corner_from) {
my @in = grep { $_->{corner} eq $c } @paths;
my @bad = grep { $_->{verdict} eq "VIOLATED" } @in;
my ($worst) = sort { $a->{slack} <=> $b->{slack} } @in;
my $tns = 0;
$tns += $_->{slack} for grep { $_->{slack} < 0 } @in;
printf "%-16s %6d %9d %9s %9.4f %s\n", $c, scalar @in, scalar @bad,
$worst->{slack}, $tns, $in[0]{format};
}
# ---- the top N, worst first; ties broken by corner then endpoint, so the
# ---- list is the same in every process (hash order is not)
my @ranked = sort { $a->{slack} <=> $b->{slack}
or $a->{corner} cmp $b->{corner}
or $a->{end} cmp $b->{end} } @paths;
my $n = $opt{top} < @ranked ? $opt{top} : scalar @ranked;
printf "\nworst %d of %d paths:\n", $n, scalar @ranked;
printf " %2d. %9s %-8s %-15s %s\n", $_ + 1, @{ $ranked[$_] }{qw(slack verdict corner end)}
for 0 .. $n - 1;
# ---- the histogram: floor() buckets, VIOLATED and MET counted apart,
# ---- because the printed number alone cannot say which side "-0.0000" is on
my (%viol, %met);
for my $p (@paths) {
my $bucket = floor($p->{slack} / $opt{bin});
($p->{verdict} eq "VIOLATED" ? $viol{$bucket} : $met{$bucket})++;
}
my @buckets = sort { $a <=> $b } keys %{ { %viol, %met } };
printf "\nslack histogram, bin %.2f ns (V = violated, m = met):\n", $opt{bin};
for my $k ($buckets[0] .. $buckets[-1]) {
my ($v, $m) = ($viol{$k} // 0, $met{$k} // 0);
printf " [%+.2f, %+.2f) %3d %3d %s%s\n", $k * $opt{bin}, ($k + 1) * $opt{bin},
$v, $m, "V" x $v, "m" x $m;
}
my $violated = grep { $_->{verdict} eq "VIOLATED" } @paths;
printf "\n%d violated path(s) in %d corner(s)\n", $violated,
scalar(keys %{ { map { $_->{corner} => 1 } grep { $_->{verdict} eq "VIOLATED" } @paths } });
exit($violated ? VIOLATED : ALL_MET);
# ---------------------------------------------------------------- readers
# Each returns (corner, paths). A path keeps the SLACK TEXT exactly as the
# report printed it: numifying "-0.0000" gives 0 and loses the sign.
sub read_primetime {
my ($file, $text) = @_;
my ($corner) = $text =~ /^Corner\s*:\s*(\S+)/m;
unless ($corner) { push @problems, "$file: PrimeTime layout with no Corner line"; return }
my @paths;
my @blocks = split /(?=^[ \t]*Startpoint:)/m, $text;
shift @blocks; # the header
for my $i (0 .. $#blocks) {
my $blk = $blocks[$i];
my %p = (corner => $corner, format => "primetime");
($p{start}) = $blk =~ /^\s*Startpoint:\s*(\S+)/m;
($p{end}) = $blk =~ /^\s*Endpoint:\s*(\S+)/m;
($p{group}) = $blk =~ /^\s*Path Group:\s*(\S+)/m;
($p{type}) = $blk =~ /^\s*Path Type:\s*(\S+)/m;
@p{qw(verdict slack)} = $blk =~ /^\s*slack \((MET|VIOLATED)\)\s+(-?\d+\.\d+)\s*$/m;
my @missing = grep { !defined $p{$_} } qw(start end group type verdict slack);
if (@missing) {
push @problems, sprintf "%s: path %d (%s) has no %s", $file, $i + 1,
$p{end} // "endpoint unknown", join("/", @missing);
next;
}
push @paths, \%p;
}
return ($corner, @paths);
}
sub read_tempus {
my ($file, $text) = @_;
my (@paths, %views);
my @blocks = split /(?=^Path \d+:)/m, $text;
shift @blocks if $blocks[0] !~ /^Path \d+:/;
for my $i (0 .. $#blocks) {
my $blk = $blocks[$i];
my %p = (format => "tempus");
my ($kind);
($p{verdict}, $kind) = $blk =~ /^Path \d+: (MET|VIOLATED) (Setup|Hold) Check/;
$p{type} = defined $kind ? ($kind eq "Setup" ? "max" : "min") : undef;
($p{end}) = $blk =~ /^Endpoint:\s+(\S+)/m;
($p{start}) = $blk =~ /^Beginpoint:\s+(\S+)/m;
($p{group}) = $blk =~ /^Path Groups:\s*\{(\S+?)\}/m;
($p{corner}) = $blk =~ /^Analysis View:\s*(\S+)/m;
($p{slack}) = $blk =~ /^= Slack Time\s+(-?\d+\.\d+)\s*$/m;
my @missing = grep { !defined $p{$_} } qw(verdict type end start group corner slack);
if (@missing) {
push @problems, sprintf "%s: path %d (%s) has no %s", $file, $i + 1,
$p{end} // "endpoint unknown", join("/", @missing);
next;
}
$views{ $p{corner} } = 1;
push @paths, \%p;
}
my @v = sort keys %views;
if (@v > 1) { push @problems, "$file: one file, several analysis views: @v"; return }
unless (@v) { push @problems, "$file: Tempus layout with no readable path"; return }
return ($v[0], @paths);
}
sub usage {
my ($why) = @_;
print STDERR "parse_sta.pl: $why\n" if $why;
print STDERR <<'END';
usage: parse_sta.pl REPORT_DIR [--top N] [--csv FILE] [--bin NS]
reads REPORT_DIR/*.rpt, each one corner, in PrimeTime or Tempus layout
exit: 0 every path met; 1 at least one VIOLATED path;
2 an input problem (nothing written); 3 usage
END
exit($why ? USAGE : ALL_MET);
}
sub fail {
my ($code, $msg) = @_;
unlink $opt{csv} if defined $opt{csv};
print STDERR "parse_sta.pl: $msg\n";
exit $code;
}Formats are a table
my %FORMAT = (
primetime => { looks => qr/^Report : timing\b/m, read => \&read_primetime },
tempus => { looks => qr/^Path \d+: (?:MET|VIOLATED) /m, read => \&read_tempus },
);Each layout has a recognition pattern and a reader, stored as a code reference (Chapter 7.11). Detection runs every pattern and requires exactly one match. A third tool is a third row plus its reader; nothing else in the program changes.
The slack is captured as text, with the verdict beside it
@p{qw(verdict slack)} = $blk =~ /^\s*slack \((MET|VIOLATED)\)\s+(-?\d+\.\d+)\s*$/m;One pattern captures the annotation and the number together, so they cannot come from different lines. The value is stored exactly as captured and is never passed through arithmetic before it reaches the CSV. It is used as a number only for ranking, bucketing and TNS, where the annotation travels with it.
Every path block is checked for completeness
Each reader fills a hash of six or seven fields and then asks which are still undefined. That one grep turns "the slack line was cut off" into a message naming the file, the path's position and the missing fields — instead of a path that silently is not there.
The ranking does not depend on anything but the data
my @ranked = sort { $a->{slack} <=> $b->{slack}
or $a->{corner} cmp $b->{corner}
or $a->{end} cmp $b->{end} } @paths;Two tie-breaks make the order total: no two paths have the same corner and endpoint, so no two compare equal. Chapter 23.10 measured five different orderings in five processes when ties were left to chance; with this sort, the order depends on the data alone, not on hash order or on the order paths happened to appear in the report. The slice is clamped so that --top 500 on 98 paths lists 98, not 98 followed by 402 empty rows.
The histogram keeps violated and met apart
for my $p (@paths) {
my $bucket = floor($p->{slack} / $opt{bin});
($p->{verdict} eq "VIOLATED" ? $viol{$bucket} : $met{$bucket})++;
}floor rather than int, because int truncates toward zero and makes the bucket at zero twice as wide — Chapter 23.10's finding. And two counters rather than one, because of section 3: the bucket at zero can legitimately contain both.
6. Running It
#!/bin/bash
# demo.sh -- generate the sample reports, parse all three corners, and
# compare every recovered value with what the generator wrote.
perl make_reports.pl sta || exit 1
ls sta/reports
echo
perl parse_sta.pl sta/reports --top 12 --csv slack.csv
echo "exit $?"
echo
echo "round trip against the generator's own values:"
perl -e '
open my $csv, "<", "slack.csv" or die $!; <$csv>;
my %got = map { chomp; my @f = split /,/; ("$f[0] $f[1]" => "$f[2] $f[4] $f[5] $f[6]") } <$csv>;
open my $exp, "<", "sta/expected.tsv" or die $!;
my ($n, $same) = (0, 0);
while (<$exp>) {
chomp; my ($c, $e, $s, $t, $slack, $v) = split /\t/;
$n++; $same++ if ($got{"$c $e"} // "") eq "$s $t $slack $v";
}
printf " %d of %d paths recovered exactly; %d rows in slack.csv\n", $same, $n, scalar keys %got;
'fast_1v10_m40c.pt.rpt
slow_0v90_125c.tempus.rpt
typ_1v00_25c.pt.rpt
CORNER PATHS VIOLATED WNS TNS FORMAT
fast_1v10_m40c 33 1 -0.0000 0.0000 primetime
slow_0v90_125c 33 11 -0.2869 -1.4387 tempus
typ_1v00_25c 32 0 0.0350 0.0000 primetime
worst 12 of 98 paths:
1. -0.2869 VIOLATED slow_0v90_125c lane3/sum_reg_7_/D
2. -0.2431 VIOLATED slow_0v90_125c lane2/sum_reg_7_/D
3. -0.1992 VIOLATED slow_0v90_125c lane0/sum_reg_7_/D
4. -0.1992 VIOLATED slow_0v90_125c lane1/sum_reg_7_/D
5. -0.1636 VIOLATED slow_0v90_125c lane3/sum_reg_6_/D
6. -0.1252 VIOLATED slow_0v90_125c lane2/sum_reg_6_/D
7. -0.0869 VIOLATED slow_0v90_125c lane0/sum_reg_6_/D
8. -0.0869 VIOLATED slow_0v90_125c lane1/sum_reg_6_/D
9. -0.0403 VIOLATED slow_0v90_125c lane3/sum_reg_5_/D
10. -0.0074 VIOLATED slow_0v90_125c lane2/sum_reg_5_/D
11. -0.0000 VIOLATED fast_1v10_m40c ctl/sync_reg_1_/D
12. -0.0000 VIOLATED slow_0v90_125c ctl/done_reg/D
slack histogram, bin 0.05 ns (V = violated, m = met):
[-0.30, -0.25) 1 0 V
[-0.25, -0.20) 1 0 V
[-0.20, -0.15) 3 0 VVV
[-0.15, -0.10) 1 0 V
[-0.10, -0.05) 2 0 VV
[-0.05, +0.00) 2 0 VV
[+0.00, +0.05) 2 3 VVmmm
[+0.05, +0.10) 0 4 mmmm
[+0.10, +0.15) 0 4 mmmm
[+0.15, +0.20) 0 4 mmmm
[+0.20, +0.25) 0 7 mmmmmmm
[+0.25, +0.30) 0 6 mmmmmm
[+0.30, +0.35) 0 9 mmmmmmmmm
[+0.35, +0.40) 0 6 mmmmmm
[+0.40, +0.45) 0 7 mmmmmmm
[+0.45, +0.50) 0 8 mmmmmmmm
[+0.50, +0.55) 0 4 mmmm
[+0.55, +0.60) 0 12 mmmmmmmmmmmm
[+0.60, +0.65) 0 4 mmmm
[+0.65, +0.70) 0 4 mmmm
[+0.70, +0.75) 0 4 mmmm
12 violated path(s) in 2 corner(s)
exit 1
round trip against the generator's own values:
98 of 98 paths recovered exactly; 98 rows in slack.csv98 of 98 paths come back exactly, from both layouts — corner, endpoint, startpoint, type, printed slack and verdict. The two -0.0000 paths are in the list as VIOLATED, one from each layout, and the zero bucket shows them as VV ahead of three met paths. Paths 3 and 4, and 7 and 8, are the tied lanes, listed by name.
One line deserves a second look: the fast corner's TNS is 0.0000 although it has a violated path. Total negative slack sums the values below zero, and a value that rounds to zero contributes nothing. That is arithmetically correct and still misleading — which is why the summary prints the VIOLATED count beside it. A violation count and a TNS answer different questions, and neither one alone is the corner's status.
7. Every Input Problem
#!/bin/bash
# inputs.sh -- one deliberately damaged report directory per case.
base=$(mktemp -d)
trap 'rm -rf "$base"' EXIT
perl make_reports.pl "$base/good" || exit 1
R=$base/good/reports
tool=$PWD/parse_sta.pl
n=0
try() { # LABEL SETUP-COMMAND [EXTRA ARGS]
local label=$1 setup=$2; shift 2
n=$((n + 1)); d=$base/$n/reports
mkdir -p "$d"
eval "$setup"
( cd "$base/$n" && perl "$tool" reports --csv out.csv "$@" > /dev/null 2> err )
local rc=$?
printf '== %s\n exit %d, csv written: %s\n' "$label" "$rc" \
"$([ -e "$base/$n/out.csv" ] && echo yes || echo no)"
grep -v -E 'nothing written|^usage:|^ |^exit:' "$base/$n/err" | sed 's/^/ /'
}
try "all three corners" 'cp $R/*.rpt "$d"'
try "the typical corner alone" 'cp $R/typ*.rpt "$d"'
try "an empty directory" ':'
try "a log file named .rpt" 'cp $R/typ*.rpt "$d"; printf "Info: run started\nInfo: done\n" > "$d/run.rpt"'
try "a report cut off mid-path" 'cp $R/fast*.rpt "$d"; head -40 $R/typ*.rpt > "$d/typ_1v00_25c.pt.rpt"'
try "one corner in two files" 'cp $R/typ*.rpt "$d"; cp $R/typ*.rpt "$d/typ_rerun.pt.rpt"'
try "a header with no paths" 'head -10 $R/typ*.rpt > "$d/typ_1v00_25c.pt.rpt"'
try "violations AND a cut-off report" 'cp $R/slow*.rpt "$d"; head -40 $R/typ*.rpt > "$d/typ_1v00_25c.pt.rpt"'
try "--top 0" 'cp $R/typ*.rpt "$d"' --top 0== all three corners
exit 1, csv written: yes
== the typical corner alone
exit 0, csv written: yes
== an empty directory
exit 2, csv written: no
parse_sta.pl: no *.rpt files in reports
== a log file named .rpt
exit 2, csv written: no
parse_sta.pl: reports/run.rpt: not a recognised timing report
== a report cut off mid-path
exit 2, csv written: no
parse_sta.pl: reports/typ_1v00_25c.pt.rpt: path 2 (lane2/sum_reg_7_/D) has no group/type/verdict/slack
== one corner in two files
exit 2, csv written: no
parse_sta.pl: reports/typ_rerun.pt.rpt: corner typ_1v00_25c was already read from reports/typ_1v00_25c.pt.rpt
== a header with no paths
exit 2, csv written: no
parse_sta.pl: reports/typ_1v00_25c.pt.rpt: no paths in corner typ_1v00_25c
== violations AND a cut-off report
exit 2, csv written: no
parse_sta.pl: reports/typ_1v00_25c.pt.rpt: path 2 (lane2/sum_reg_7_/D) has no group/type/verdict/slack
== --top 0
exit 3, csv written: no
parse_sta.pl: --top must be at least 1Nine cases, nine expected outcomes. Six input problems, each with its own message, and no CSV after any of them. The case that shows the precedence rule is the last but one: the slow corner's eleven violations are real, and the run still exits 2, because the typical corner's report was cut off after its first path. Exiting 1 there would publish a verdict over a corner nobody read.
The cut-off case also shows how a message should name a problem: the file, the path's position in it (path 2), the endpoint if it was read, and exactly which fields are missing (group/type/verdict/slack). The engineer who reads it knows the report was truncated, and where, without opening it.
8. A Test Suite For The Tool
#!/usr/bin/perl
# t/parse_sta.t -- black-box tests for parse_sta.pl over generated reports.
use strict;
use warnings;
use Test::More;
use File::Temp qw(tempdir);
use File::Copy qw(copy);
use FindBin qw($Bin);
my $TOOL = "$Bin/../parse_sta.pl";
my $base = tempdir(CLEANUP => 1);
system($^X, "$Bin/../make_reports.pl", "$base/gen") == 0 or die "make_reports.pl failed\n";
my $R = "$base/gen/reports";
my ($TYP, $SLOW, $FAST) = map { (glob("$R/$_*"))[0] } qw(typ slow fast);
sub slurp { open my $fh, "<", $_[0] or return undef; local $/; return scalar <$fh> }
sub spew { open my $fh, ">", $_[0] or die "$_[0]: $!"; print {$fh} $_[1]; close $fh }
my $n = 0;
sub case_dir { # a fresh report directory holding copies of FILES
my $d = "$base/case" . ++$n;
mkdir $d or die;
copy($_, $d) or die "copy $_: $!" for @_;
return $d;
}
# Run the tool; return (status, stdout, stderr). %env is added to %ENV.
sub run_tool {
my ($dir, $env, @extra) = @_;
my $pid = fork // die "fork: $!";
if ($pid == 0) {
@ENV{ keys %$env } = values %$env;
open STDOUT, ">", "$base/out" or die;
open STDERR, ">", "$base/err" or die;
exec $^X, $TOOL, $dir, "--csv", "$dir.csv", @extra;
die "exec: $!";
}
waitpid $pid, 0;
return ($? >> 8, slurp("$base/out") // "", slurp("$base/err") // "");
}
# Runs that are compared with each other share one hash seed, so that the
# only thing that differs between them is the thing the test is about.
my %PIN = (PERL_HASH_SEED => 7, PERL_PERTURB_KEYS => 2);
# ---- the round trip: every value the generator wrote comes back exactly
my $all = case_dir($TYP, $SLOW, $FAST);
my ($rc, $out, $err) = run_tool($all, \%PIN, "--top", 12);
is($rc, 1, "three corners, one with violations: exit 1");
my %got;
for (split /\n/, slurp("$all.csv")) {
my @f = split /,/;
$got{"$f[0] $f[1]"} = "$f[2] $f[4] $f[5] $f[6]";
}
my ($paths, $same) = (0, 0);
for (split /\n/, slurp("$base/gen/expected.tsv")) {
my ($c, $e, $s, $type, $slack, $v) = split /\t/;
$paths++;
$same++ if ($got{"$c $e"} // "") eq "$s $type $slack $v";
}
is($same, $paths, "all $paths paths recovered exactly, from both layouts");
is($got{"slow_0v90_125c ctl/done_reg/D"}, "ctl/cnt_reg_3_/Q max -0.0000 VIOLATED",
"Tempus layout: the -0.0000 setup path keeps its sign and its VIOLATED verdict");
is($got{"fast_1v10_m40c ctl/sync_reg_1_/D"}, "ctl/sync_reg_0_/Q min -0.0000 VIOLATED",
"PrimeTime layout: the -0.0000 hold path keeps its sign and its VIOLATED verdict");
like($out, qr/^ \[\+0\.00, \+0\.05\) 2 3 VVmmm$/m,
"the histogram shows both violated paths in the zero bucket");
# ---- exit status follows the verdicts
($rc) = run_tool(case_dir($TYP), {});
is($rc, 0, "the typical corner alone: every path met, exit 0");
# ---- the ranking does not depend on the order paths appear in a report
my @blocks = split /(?=^Path \d+:)/m, slurp($SLOW);
my $header = shift @blocks;
my $rev = case_dir($TYP, $FAST);
spew("$rev/" . (split m{/}, $SLOW)[-1], $header . join("", reverse @blocks));
my (undef, $out_rev) = run_tool($rev, \%PIN, "--top", 12);
is($out_rev, $out, "reversing the order of paths in a report changes nothing");
# ---- nor on the process: three different hash seeds, one output
my %outputs;
for my $seed (1 .. 5) {
my (undef, $o) = run_tool($all, { PERL_HASH_SEED => $seed, PERL_PERTURB_KEYS => 2 }, "--top", 12);
$outputs{$o}++;
}
is(scalar keys %outputs, 1, "five hash seeds give one identical report");
# ---- asking for more than exists
($rc, $out, $err) = run_tool($all, {}, "--top", 500);
ok($out =~ /^worst 98 of 98 paths:/m && $err eq "", "--top 500 lists all 98, with nothing on STDERR");
# ---- every input problem: exit 2, a reason, and no CSV
my $cut = case_dir($FAST);
spew("$cut/typ_1v00_25c.pt.rpt", join("", (split /^/m, slurp($TYP))[0 .. 39]));
($rc, undef, $err) = run_tool($cut, {});
ok($rc == 2 && $err =~ /path 2 \(lane2\/sum_reg_7_\/D\) has no/ && !-e "$cut.csv",
"a report cut off mid-path: exit 2, the path named, no CSV");
my $log = case_dir($TYP);
spew("$log/run.rpt", "Info: run started\n");
($rc, undef, $err) = run_tool($log, {});
ok($rc == 2 && $err =~ /run\.rpt: not a recognised timing report/, "a log named .rpt: exit 2");
my $dup = case_dir($TYP);
copy($TYP, "$dup/typ_rerun.pt.rpt") or die;
($rc, undef, $err) = run_tool($dup, {});
ok($rc == 2 && $err =~ /corner typ_1v00_25c was already read/, "one corner in two files: exit 2");
my $both = case_dir($SLOW);
spew("$both/typ_1v00_25c.pt.rpt", join("", (split /^/m, slurp($TYP))[0 .. 39]));
($rc) = run_tool($both, {});
is($rc, 2, "violations and a cut-off report: the input problem wins, exit 2");
my $empty = case_dir();
($rc) = run_tool($empty, {});
is($rc, 2, "an empty directory: exit 2");
($rc) = run_tool($all, {}, "--top", 0);
is($rc, 3, "--top 0: exit 3");
done_testing();Test 2 is the round trip, and it is what the generator's truth table is for: every path the generator wrote must come back with the same corner, startpoint, type, printed slack and verdict.
Tests 7 and 8 check two different kinds of determinism. Test 7 reverses the order of the paths inside the Tempus report and requires identical output. Test 8 runs the parser under five different PERL_HASH_SEED values, each of which gives the program a different hash order (Chapter 6.6). A ranking that leans on input order fails the first; a report that walks a hash without sorting fails the second. The two runs test 7 compares share one pinned seed, so the only thing that differs between them is the input order — change one variable per test, or a failure cannot say which one mattered.
Test 13 pins the precedence rule from the brief: violations plus a cut-off report must exit 2.
#!/bin/bash
# tests.sh -- run the suite through prove, keeping prove's own exit status.
prove -v t/ > prove.log 2>&1
status=$?
grep -v 'wallclock' prove.log
echo "prove exit $status"t/parse_sta.t ..
ok 1 - three corners, one with violations: exit 1
ok 2 - all 98 paths recovered exactly, from both layouts
ok 3 - Tempus layout: the -0.0000 setup path keeps its sign and its VIOLATED verdict
ok 4 - PrimeTime layout: the -0.0000 hold path keeps its sign and its VIOLATED verdict
ok 5 - the histogram shows both violated paths in the zero bucket
ok 6 - the typical corner alone: every path met, exit 0
ok 7 - reversing the order of paths in a report changes nothing
ok 8 - five hash seeds give one identical report
ok 9 - --top 500 lists all 98, with nothing on STDERR
ok 10 - a report cut off mid-path: exit 2, the path named, no CSV
ok 11 - a log named .rpt: exit 2
ok 12 - one corner in two files: exit 2
ok 13 - violations and a cut-off report: the input problem wins, exit 2
ok 14 - an empty directory: exit 2
ok 15 - --top 0: exit 3
1..15
ok
All tests successful.
Result: PASS
prove exit 09. Proving The Suite Can Fail
#!/bin/bash
# mutate.sh -- plant one defect at a time in a COPY of the tool; does the
# suite notice?
mutant() { # LABEL PERL-SUBSTITUTION (applied to the whole file)
local label=$1 edit=$2 d failed
d=$(mktemp -d)
mkdir "$d/t"
cp make_reports.pl "$d/"
cp t/parse_sta.t "$d/t/"
perl -0pe "$edit" parse_sta.pl > "$d/parse_sta.pl"
if cmp -s parse_sta.pl "$d/parse_sta.pl"; then
printf '%-46s NOT PLANTED (edit matched nothing)\n' "$label"
elif ! perl -c "$d/parse_sta.pl" > /dev/null 2>&1; then
printf '%-46s INVALID (the mutant does not compile)\n' "$label"
else
failed=$(perl "$d/t/parse_sta.t" 2>/dev/null | grep -c '^not ok')
printf '%-46s %s (%s failing test(s))\n' "$label" \
"$([ "$failed" -gt 0 ] && echo CAUGHT || echo SURVIVED)" "$failed"
fi
rm -rf "$d"
}
mutant "1 verdict from the sign, not the annotation" \
's/(\@p\{qw\(verdict slack\)\} = .*?\n)/$1 \$p{verdict} = \$p{slack} < 0 ? "VIOLATED" : "MET" if defined \$p{slack};\n/'
mutant "2 re-format the slack as a number" \
's/(\(\$p\{slack\}\) = \$blk =~ .*?\n)/$1 \$p{slack} = sprintf "%.4f", \$p{slack} + 0 if defined \$p{slack};\n/'
mutant "3 int() instead of floor()" \
's/my \$bucket = floor\(/my \$bucket = int(/'
mutant "4 no tie-break in the ranking" \
's/\n\s+or \$a->\{corner\} cmp \$b->\{corner\}\n\s+or \$a->\{end\} cmp \$b->\{end\} \} \@paths;/ } \@paths;/'
mutant "5 report corners in hash order" \
's/for my \$c \(sort keys %corner_from\)/for my \$c (keys %corner_from)/'
mutant "6 do not clamp --top" \
's/my \$n = \$opt\{top\} < \@ranked \? \$opt\{top\} : scalar \@ranked;/my \$n = \$opt{top};/'
mutant "7 allow one corner twice" \
's/if \(my \$prev = \$corner_from\{\$corner\}\)/if (my \$prev = \$corner_from{"never"})/'
mutant "8 violations outrank input problems" \
's/INPUT => 2/INPUT => 1/'1 verdict from the sign, not the annotation CAUGHT (3 failing test(s))
2 re-format the slack as a number CAUGHT (2 failing test(s))
3 int() instead of floor() CAUGHT (1 failing test(s))
4 no tie-break in the ranking CAUGHT (1 failing test(s))
5 report corners in hash order CAUGHT (1 failing test(s))
6 do not clamp --top CAUGHT (1 failing test(s))
7 allow one corner twice CAUGHT (1 failing test(s))
8 violations outrank input problems CAUGHT (5 failing test(s))All eight are caught now. On the first run, mutant 1 survived. It replaces the PrimeTime reader's verdict with a sign test — the exact mistake Chapter 23.3 warned about — and no test noticed, because the only path in the sample where sign and annotation disagreed was in the Tempus corner. The PrimeTime reader had never been shown a -0.0000 VIOLATED. The fix was in the generator, not the tests: a hold check in the fast corner that fails by less than the last printed digit. It is a realistic path — hold problems appear at the fast corner — and now each reader meets the case that matters.
Mutant 4 removes the tie-break and is caught only by test 7, the reversed report. With the tie-break gone, the order of tied paths follows the order they were read in, which test 8's hash seeds cannot change. It took a test that changes the input order to see it.
10. What The Tool Does Not Do
- It reads summary fields only. It does not reconcile the point table's
IncrandPathcolumns — Chapter 23.6's check — so a report whose arithmetic is internally inconsistent is accepted. - It knows two layouts, as Chapters 23.6 and 23.7 described them. A real tool version that prints a field differently is a new row in the format table and a new sample in the generator, before anything else.
- It treats one file as one corner. A report that covers several analysis views in one file is refused, not split.
- It keeps every path in memory. That is right for the few thousand paths a report usually holds after
-max_paths; a full-design dump of every endpoint wants the streaming shape of Chapter 18.10. - Its TNS is limited by the printed precision, as the fast corner showed.
11. Exercises
1. Report the precedence. When the run exits 2 and some readable corner did contain violations, the engineer would like to know. Add a single STDERR line in that case, without writing the CSV or changing the exit status. Which test do you add?
2. A third layout. Invent a simple layout — one path per line, endpoint slack verdict — add it to the generator for a fourth corner, add a format row and a reader, and keep the round trip at 100%.
3. Per-corner histograms. Add --by-corner to print one histogram per corner. Does the zero-bucket problem appear in each one that has a -0.0000 path?
4. A worse sort. Write a ranking that sorts only by slack and endpoint (not corner). Find the input on which it gives a different top-12 from the tool's, and turn that input into a test.
5. The arithmetic check. Add --check-arithmetic, which reconciles each PrimeTime block's data arrival time with the sum of its Incr column (Chapter 23.6). Which mutant of the generator proves your check can fire?
12. What To Remember
- Exit 2 outranks exit 1: an incomplete read cannot certify anything, so input problems are reported first and nothing is written.
- Detect the layout before reading a field, and require exactly one match.
- Keep the slack as printed text. Measured:
"-0.0000" + 0prints as0, and every number formatted from it afterwards prints0.0000. - The verdict comes from the annotation, in both layouts.
- A path missing any field stops the run, and the message names the file, the position and the fields.
- Make the ranking total with tie-breaks, and test it by changing the input order and the hash seed — in separate tests.
- Count violated and met separately in each histogram bucket. The bucket at zero held both.
- A round trip against the generator's own truth table is the strongest test a parser of sample data can have. Measured: 98 of 98.
- A survivor pointed at the fixture again: the PrimeTime reader had never met
-0.0000 VIOLATED.
Where this fits
Part of the Perl curriculum.
