Perl Part II · Module 23 · Static Timing Analysis Report Parsing
Normalising Reports Before Downstream Tools
Every chapter so far has read a report. This one writes one — a single record per path with fixed fields, produced from either tool's output, so that everything downstream stops caring which tool ran. The interesting part is not the writing but the proof: the only way to know a normaliser works is to feed it the same design in two formats and check that what comes out the other side is identical.
Intermediate11 min readPerlPart IISTA Reports
Part II · Module 23 · Chapter 23.8 · Normalising Reports Before Downstream Tools
1. Three Files, Two Formats, Twelve Records
use strict;
use warnings;
# Reads BOTH formats and emits ONE record per path, so a downstream consumer
# never learns which tool produced the input. The PrimeTime reports are
# generated from the real Module 20 netlist; the Tempus file is the same
# numbers re-laid-out (23.7). Neither tool exists here; see 23.1.
my @INPUTS = qw(
reports/typ_1v00_25c.pt.rpt
reports/slow_0v90_125c.pt.rpt
reports/slow_0v90_125c.tempus.rpt
);
# ---- per-format patterns, selected after DETECTING the format (23.7)
my %FORMAT = (
primetime => {
block => qr/(?=^[ \t]*Startpoint:)/m,
start => qr/^[ \t]*Startpoint:[ \t]*(\S+)/m,
endpoint => qr/^[ \t]*Endpoint:[ \t]*(\S+)/m,
group => qr/^[ \t]*Path Group:[ \t]*(\S+)/m,
type => qr/^[ \t]*Path Type:[ \t]*(\S+)/m,
slack => qr/^[ \t]*slack[ \t]*(?:\((\w+)\))?[ \t]*(-?[\d.]+|INFINITY)[ \t]*$/m,
},
tempus => {
block => qr/(?=^Path \d+:)/m,
start => qr/^Beginpoint:[ \t]+(\S+)/m,
endpoint => qr/^Endpoint:[ \t]+(\S+)/m,
group => qr/^Path Groups:[ \t]*\{([^}]*)\}/m,
type => undef, # not printed; inferred below
slack => qr/^=[ \t]*Slack Time[ \t]+(-?[\d.]+)/m,
verdict => qr/^Path \d+:[ \t]*(\w+)[ \t]+(\w+)[ \t]+Check/m,
},
);
sub detect {
my ($t) = @_;
return "tempus" if $t =~ /^Path \d+:.*Check/m;
return "primetime" if $t =~ /^[ \t]*Startpoint:/m;
return undef; # an unknown format is an ERROR
}
sub corner_of {
my ($t) = @_;
my ($c) = $t =~ /^Corner[ \t]*:[ \t]*(\S+)/m;
($c) = $t =~ /^Analysis View:[ \t]*(\S+)/m unless defined $c;
return $c;
}
my @records;
my %seen_format;
for my $f (@INPUTS) {
open my $fh, "<", $f or die "$f: $!";
local $/;
my $text = <$fh>;
close $fh;
my $fmt = detect($text);
unless (defined $fmt) {
warn "error: unrecognised timing report format in $f\n";
next;
}
$seen_format{$fmt}++;
my $P = $FORMAT{$fmt};
my $corner = corner_of($text) // "unknown";
my @blocks = split $P->{block}, $text;
shift @blocks while @blocks && $blocks[0] !~ ($fmt eq "tempus" ? qr/^Path \d+:/ : qr/Startpoint:/);
for my $b (@blocks) {
my %r = (format => $fmt, corner => $corner, source => ($f =~ m{([^/]+)$})[0]);
$r{start} = ($b =~ $P->{start})[0];
$r{endpoint} = ($b =~ $P->{endpoint})[0];
($r{group}) = $b =~ $P->{group};
$r{group} =~ s/[{}]//g if defined $r{group}; # 23.7: Tempus braces
if ($fmt eq "primetime") {
my ($ann, $val) = $b =~ $P->{slack};
@r{qw(verdict slack)} = ($ann, $val);
$r{type} = ($b =~ $P->{type})[0];
} else {
($r{slack}) = $b =~ $P->{slack};
my ($v, $chk) = $b =~ $P->{verdict};
$r{verdict} = $v;
$r{type} = (defined $chk && lc($chk) eq "hold") ? "min" : "max";
}
next unless defined $r{endpoint} && defined $r{slack};
push @records, \%r;
}
}
printf "read %d record(s) from %d file(s): %s\n\n", scalar @records, scalar @INPUTS,
join(", ", map { "$_ x$seen_format{$_}" } sort keys %seen_format);
# ---- ONE record per line, fixed fields, no tool-specific anything
my @FIELDS = qw(corner endpoint start group type verdict slack);
print join("\t", @FIELDS), "\n";
for my $r (@records) {
print join("\t", map { defined $r->{$_} ? $r->{$_} : "-" } @FIELDS), "\n";
}
# ---- the property that makes it normalised: the SAME path from two formats
print "\nthe same endpoint read from both formats:\n";
my %by_key;
push @{ $by_key{"$_->{corner}|$_->{endpoint}"} }, $_ for @records;
for my $k (sort keys %by_key) {
my @g = @{ $by_key{$k} };
next unless @g > 1;
my %fmts = map { $_->{format} => 1 } @g;
next unless keys %fmts > 1;
printf " %-34s from %s\n", $k, join(" and ", sort keys %fmts);
my %vals = map { $_->{slack} => 1 } @g;
printf " slack values seen: %s -> %s\n", join(", ", sort keys %vals),
(keys %vals == 1 ? "identical" : "DIFFER");
}
# ---- and what normalising removes
print "\nwhat the normalised record does NOT contain:\n";
print " no banner, no page marker, no column layout, no tool name in a\n";
print " field, and no line whose meaning depends on the line above it.\n";
printf " a downstream consumer sees %d fields and never learns which tool\n", scalar @FIELDS;
print " produced the input -- which is the whole point (22.1's 'turn a\n";
print " parsing job into an extraction job', one level further along).\n";read 12 record(s) from 3 file(s): primetime x2, tempus x1
corner endpoint start group type verdict slack
typ_1v00_25c bit0/D bit0/Q clk max MET 0.7290
typ_1v00_25c bit1/D bit0/Q clk max MET 0.6530
typ_1v00_25c bit2/D bit1/Q clk max MET 0.5800
typ_1v00_25c bit3/D bit1/Q clk max MET 0.5070
slow_0v90_125c bit0/D bit0/Q clk max MET 0.4650
slow_0v90_125c bit1/D bit0/Q clk max MET 0.2750
slow_0v90_125c bit2/D bit1/Q clk max MET 0.0925
slow_0v90_125c bit3/D bit1/Q clk max VIOLATED -0.0900
slow_0v90_125c bit0/D bit0/Q clk max MET 0.4650
slow_0v90_125c bit1/D bit0/Q clk max MET 0.2750
slow_0v90_125c bit2/D bit1/Q clk max MET 0.0925
slow_0v90_125c bit3/D bit1/Q clk max VIOLATED -0.0900
the same endpoint read from both formats:
slow_0v90_125c|bit0/D from primetime and tempus
slack values seen: 0.4650 -> identical
slow_0v90_125c|bit1/D from primetime and tempus
slack values seen: 0.2750 -> identical
slow_0v90_125c|bit2/D from primetime and tempus
slack values seen: 0.0925 -> identical
slow_0v90_125c|bit3/D from primetime and tempus
slack values seen: -0.0900 -> identical
what the normalised record does NOT contain:
no banner, no page marker, no column layout, no tool name in a
field, and no line whose meaning depends on the line above it.
a downstream consumer sees 7 fields and never learns which tool
produced the input -- which is the whole point (22.1's 'turn a
parsing job into an extraction job', one level further along).corner endpoint start group type verdict slack
typ_1v00_25c bit3/D bit1/Q clk max MET 0.5070
slow_0v90_125c bit3/D bit1/Q clk max VIOLATED -0.0900Seven fields, tab-separated, one line per path. No banner, no columns, no tool name — and the same shape whichever tool produced the input.
Tab-separated for Chapter 13.14's reason: an endpoint path can contain almost any character, and it will not contain a tab. CSV would need quoting rules for a field like core0/mux[3]/D.
2. The Pipeline, And Detecting Before Parsing
Figure 23.8.1 — Normalising two formats into one record
Step one is the one that is usually missing, and Chapter 23.7 measured why it matters: a PrimeTime parser pointed at a Tempus report recovers a slack with no annotation and then misclassifies a hold violation as met. So detection comes first and refusal is a real outcome:
my $fmt = detect($text);
unless (defined $fmt) {
warn "error: unrecognised timing report format in $f\n";
next;
}Step two is the corner, read from the header before any block is split — Chapter 23.1 measured that no path block carries it, and the normalised record would be unusable without it.
Steps three to five are per-format, driven by the %FORMAT table rather than by if ($fmt eq ...) scattered through the code. Chapter 18.8's table structure, and the payoff is that adding a third tool is a table entry:
my %FORMAT = (
primetime => { block => qr/(?=^[ \t]*Startpoint:)/m, slack => qr/.../, ... },
tempus => { block => qr/(?=^Path \d+:)/m, slack => qr/.../, ... },
);Two cleanups in step five are worth naming because both were found by measurement:
Tempus's braces. s/[{}]//g on the group, because Chapter 23.7 measured that (\S+) keeps them and {clk} would become a second hash key alongside clk.
The inferred Path Type. Tempus does not print it, so it is derived from the check name in the path header — Setup Check becomes max, Hold Check becomes min. That is an inference and the record should be honest about it, which section 4 covers.
3. The Proof
A parser that runs is not a normaliser that works. The measurement proves it the only way available:
the same endpoint read from BOTH formats:
slow_0v90_125c|bit0/D from primetime and tempus -> identical
slow_0v90_125c|bit1/D from primetime and tempus -> identical
slow_0v90_125c|bit2/D from primetime and tempus -> identical
slow_0v90_125c|bit3/D from primetime and tempus -> identicalFour endpoints appear in both a PrimeTime and a Tempus report, and all four come out identical. Same corner, same startpoint, same group, same type, same verdict, same slack.
That is the test worth building, and it generalises: the way to verify a normaliser is to give it the same facts in every format it claims to support and compare the outputs to each other. Not to a hand-written expectation — to each other, which needs no separate source of truth.
It is the same argument Chapter 20.4 made with three independent cell counts and Chapter 22.1 with three reports agreeing: a transformation verified only against its own assumptions is not verified. Here the second format is the independent check.
4. What To Remove, And What Not To
Remove: banners, version strings, page markers, column padding, separator rules, the tool's own name, and any line whose meaning depends on the line above it. Chapter 18.3 is the general treatment; here it is what turns a layout into a record.
Do not remove three things that are tempting:
The verdict, in favour of the sign. Chapter 23.3 measured that they disagree on 4 of 6 endpoints. The normalised record carries verdict as its own field precisely so a downstream consumer never has to re-derive it.
The distinction between absent and unknown. A field the format does not print is not the same as a field that failed to parse. The script emits - for an undefined field, and that is a compromise worth being explicit about: a consumer cannot tell - meaning "Tempus does not print this" from - meaning "the pattern missed it". A production normaliser carries a separate status column, or omits the record entirely and counts it, as Chapter 21.11's per-test table does.
The fact that a value was inferred. type for a Tempus record is derived from the check name, not read from a field. A record that looks identical to one where the tool printed it is lying slightly, and Chapter 23.5 drew the same line about a generated clock's derived period: record provenance for anything computed, because the number is not in the file somebody will check it against.
So the honest record set is wider than seven columns — type_source, parse_status — and the measurement here uses seven to keep the output readable. Worth saying rather than implying.
5. What To Remember
- Normalising is what lets everything downstream stop caring which tool ran.
- Measured: 12 records from 3 files in 2 formats, one line per path, seven fixed fields.
- Tab-separated, not comma — an endpoint can contain almost anything but not a tab (13.14).
- Detect the format first and refuse if unknown (23.7) — a wrong parser misclassifies hold violations.
- Read the corner from the header before splitting blocks (23.1); no block carries it.
- Keep the per-format patterns in a table, so a third tool is a table entry (18.8).
- Strip Tempus's braces or
{clk}becomes a second group key (23.7). - Infer
Path Typefrom the check name where the tool does not print it — and record that it was inferred. - Measured: 4 endpoints read from BOTH formats came out identical in every field.
- That is the proof — a parser that runs is not a normaliser that works.
- Verify a normaliser by comparing its outputs to EACH OTHER, which needs no separate source of truth (20.4, 22.1).
- Group on
corner|endpoint— on the endpoint alone, two corners' correct answers look like a disagreement (23.9, 22.4). - Remove banners, versions, page markers, padding, rules, and the tool name (18.3).
- Do not drop the verdict in favour of the sign — they disagree on 4 of 6 endpoints (23.3).
- Do not conflate absent with unparsed —
-for both is a compromise; carry a status column. - Record provenance for anything inferred (23.5's derived clock period) — the value is not in the file a reviewer will check.
Chapter 23.9 takes the normalised records and stores them. One two-level hash answers both questions a timing review asks, and the chapter is mostly about why the two levels have to be in that order.
Where this fits
Part of the Perl curriculum.
