Skip to content
VLSI Mentor

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

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

Seven 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

A six-step normalising pipeline: detect the report format and refuse if it is unknown, read the corner from the report header, split path blocks using the format's own delimiter, extract fields using the format's own patterns, clean tool-specific decoration such as braces, and emit fixed tab-separated fields.Detect, then parse1Detect the formatrefuse if unknown2Corner from headerno block carries it3Split blocksthe format's delimiter4Extract fieldsthe format's patterns5Strip decorationbraces, padding, units6Emit fixed fieldsone TSV record per path
The first step is the one usually missing, and the refusal beside it makes the rest safe: a report whose format cannot be identified is an error, not an input to whichever parser was written first. Everything between detect and emit is per-format, driven by a table rather than by branching code, and the corner is read from the header before any block is split, because no path block carries it. The emit step produces fixed fields in a fixed order — all a downstream consumer ever sees.

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:

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

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

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

Four 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 Type from 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.