Skip to content
VLSI Mentor

Perl Part II · Module 16 · Perl in Semiconductor Engineering

What Makes a Flow Script Trustworthy

Everything built in the rest of Part II is a component in somebody's flow, which means it will be called by a Makefile, its output will be read by another program, and its exit status will decide whether a build continues. A script can be well-written Perl and still be useless in that role — and this chapter measures exactly that case, because it is the one worth understanding. Three sample scripts are scored against five conditions. One meets all five, one meets none, and the interesting one meets two: it is competent, idiomatic, would pass a code review, and cannot safely be put in a flow. Agreeing on the five conditions now is what makes every later module build tools rather than scripts.

Foundation13 min readPerlPart IIEDA Flows

Part II · Module 16 · Chapter 16.10 · What Makes a Flow Script Trustworthy

1. The Five Conditions

Figure 16.10.1 — The contract, in three groups

Five conditions for a trustworthy flow script, grouped into three: declared inputs, a machine-readable result with an honest exit status, and handled failures with reproducible output.a flow componentnot just a scriptdeclared inputscallable by anyonereadable resultcolumns + honest statustrustworthyfailures handled, stable12
Five conditions, grouped by what they protect. Declared inputs mean the script can be called correctly by somebody who did not write it. A machine-readable result and an honest exit status mean the next step in the flow can act on what happened. Handled failures and reproducible output mean the answer can be trusted and compared against yesterday's. All five are enumerated and measured in the sections below.

The five, stated plainly:

  1. Defined inputs. Every input is named on the command line or read from the environment with an explicit default. Nothing is hard-coded.
  2. A result another program can read. Output in columns with a stable separator, not sentences.
  3. An exit code that tells the truth. Distinguishing success, a failed verdict, and "could not reach a verdict".
  4. Handled failures. Every open, every close, every system checked, with the reason reported.
  5. Output that does not change between identical runs. No timestamps, no process ids, no hash-order dependence.

2. Three Scripts, Scored

Azvya Education Pvt. Ltd.VLSI Mentor
contract-check.pl
use strict;
use warnings;
use File::Temp qw(tempdir);
use File::Spec;

my $dir = tempdir(CLEANUP => 1);

# ---- three SAMPLE scripts, written for this chapter
my %script = (
"good.pl" => <<'E',
#!/usr/bin/perl
use strict; use warnings; use Getopt::Long;
my %opt = (threshold => 0.0);
GetOptions(\%opt, "input=s", "threshold=f", "help") or do { print STDERR "usage\n"; exit 2 };
unless (defined $opt{input} && -f $opt{input}) { print STDERR "error: --input FILE required\n"; exit 2 }
open my $fh, "<", $opt{input} or do { print STDERR "cannot read $opt{input}: $!\n"; exit 3 };
my (%worst, $rows);
while (my $l = <$fh>) {
    next if $l =~ /\A\s*(?:#|\z)/;
    my ($clk, $pin, $slack) = split ' ', $l;
    unless (defined $slack && $slack =~ /\A-?\d+(?:\.\d+)?\z/) { print STDERR "skip\n"; next }
    $rows++;
    $worst{$clk} = [$pin, $slack] if !exists $worst{$clk} || $slack < $worst{$clk}[1];
}
close $fh or do { print STDERR "read error: $!\n"; exit 3 };
for my $c (sort keys %worst) { printf "%s\t%s\t%.3f\n", $c, @{$worst{$c}} }
exit 3 unless $rows;
exit(grep { $worst{$_}[1] < $opt{threshold} } keys %worst) ? 1 : 0;
E
"middling.pl" => <<'E',
#!/usr/bin/perl
use strict; use warnings;
my $f = shift or die "usage: $0 FILE\n";
open my $fh, "<", $f or die "cannot read $f: $!\n";
my %w;
while (my $l = <$fh>) {
    my ($clk, $pin, $slack) = split ' ', $l;
    next unless defined $slack;
    $w{$clk} = [$pin, $slack] if !exists $w{$clk} || $slack < $w{$clk}[1];
}
close $fh;
print "Report generated ", scalar(localtime), "\n";
for my $c (sort keys %w) { print "  worst in $c is $w{$c}[0] at $w{$c}[1]\n" }
E
"bad.pl" => <<'E',
#!/usr/bin/perl
my $f = "/proj/chipA/reports/setup.rpt";
open(F, $f);
my %w;
while (<F>) { my @a = split; $w{$a[0]} = $a[2] if !$w{$a[0]} || $a[2] < $w{$a[0]} }
foreach (keys %w) { print "worst for $_ is $w{$_} at ", scalar(localtime), "\n" }
print "DONE\n";
E
);
for my $n (sort keys %script) {
    open my $w, ">", File::Spec->catfile($dir, $n) or die "cannot write: $!\n";
    print $w $script{$n};
    close $w;
}

# ---- the five conditions, as a crude SOURCE scan. It cannot know whether an
#      exit code is honest, only that more than one distinct exit exists.
my @cond = (
 [ "C1 declared inputs",     sub { ($_[0] =~ /GetOptions|\bshift\b|\@ARGV/)
                                   && $_[0] !~ m{open[^\n]*["']/\w} } ],
 [ "C2 readable result",     sub { $_[0] =~ /\\t/ } ],
 [ "C3 honest exit code",    sub { (() = $_[0] =~ /\bexit\b/g) >= 2 } ],
 [ "C4 failures handled",    sub { $_[0] =~ /\bor\s+(?:die|do)\b/ && $_[0] =~ /\$!/ } ],
 [ "C5 reproducible output", sub { $_[0] !~ /localtime|\brand\b|\$\$/ } ],
);

printf "%-14s %s TOTAL\n", "SCRIPT", join("", map { sprintf "%-6s", (split ' ', $_->[0])[0] } @cond);
for my $n (qw(good.pl middling.pl bad.pl)) {
    my $src = $script{$n};
    my ($pass, @cell) = (0);
    for my $c (@cond) {
        my $ok = $c->[1]->($src) ? 1 : 0;
        $pass += $ok;
        push @cell, sprintf("%-6s", $ok ? "yes" : "NO");
    }
    printf "%-14s %s %d of 5\n", $n, join("", @cell), $pass;
}

# ---- and prove the two that matter actually behave as scored
print "\nwhat the flow sees:\n";
open my $d, ">", File::Spec->catfile($dir, "sta.rpt") or die "cannot write: $!\n";
print $d "clk_core U1/Q 0.44\nclk_core U2/D -0.03\nclk_ddr U3/X 0.12\n";
close $d;

my $g = `cd '$dir' && perl good.pl --input sta.rpt 2>/dev/null`;
my $grc = $? >> 8;
$g =~ s/\t/<TAB>/g; $g =~ s/\n/ | /g; $g =~ s/ \| \z//;
printf "   good.pl     exit %d  stdout: %s\n", $grc, $g;

my $m = `cd '$dir' && perl middling.pl sta.rpt 2>/dev/null`;
my $mrc = $? >> 8;
my @ml = split /\n/, $m;
printf "   middling.pl exit %d  stdout line 1: %s\n", $mrc,
    ($ml[0] =~ s/ \w\w\w \w\w\w\s+\d+ [\d:]+ \d+\z/ <A TIMESTAMP>/r);
printf "               -> exit %d means SUCCESS to make, even though a\n", $mrc;
print  "                  negative-slack domain was found\n";
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
SCRIPT         C1    C2    C3    C4    C5     TOTAL
good.pl        yes   yes   yes   yes   yes    5 of 5
middling.pl    yes   NO    NO    yes   NO     2 of 5
bad.pl         NO    NO    NO    NO    NO     0 of 5

what the flow sees:
   good.pl     exit 1  stdout: clk_core<TAB>U2/D<TAB>-0.030 | clk_ddr<TAB>U3/X<TAB>0.120
   middling.pl exit 0  stdout line 1: Report generated <A TIMESTAMP>
               -> exit 0 means SUCCESS to make, even though a
                  negative-slack domain was found

Three scores: 5 of 5, 2 of 5, and 0 of 5.

bad.pl is easy to dismiss — a hard-coded path, a bareword filehandle, an unchecked open, prose output, a timestamp, no exit at all. Nobody defends it.

good.pl meets the contract, and the run at the bottom shows what that buys: tab-separated columns a later step can split, and an exit status of 1 meaning "a violation was found".

3. The Middling Script Is the Lesson

middling.pl is the one worth your attention, because it is good Perl.

It uses strict and warnings. It takes its input from the command line. It checks the open and reports $!. It uses a three-argument open and a lexical filehandle. It would pass a code review by anyone reading it as Perl.

And it fails three of the five conditions:

C2, readable result — it prints prose. worst in clk_core is U2/D at -0.03 is a sentence. A later step wanting the number has to parse English, and the moment somebody improves the wording, that step breaks. Columns with a tab are not prettier; they are parseable.

C3, honest exit code — there is no exit at all. The script runs off the end, which Chapter 15.4 measured as exit 0. So it reports success whether or not it found a negative-slack domain, and the run at the bottom of the output shows exactly that: a violation found, and make told everything was fine. This is the single most consequential defect of the three.

C5, reproducible output — it prints localtime. Two runs over identical input produce different output. You cannot diff today's report against yesterday's to see what changed, because every line after the first is offset by a header that always differs. This is why Chapter 13.5's reproducibility point keeps returning, and why every measured output in this entire track has its timestamps masked.

So the summary is: a script can be well-written and untrustworthy, and the three things it is missing are all about being a component rather than a program. That distinction is the whole point of the contract.

4. Each Condition, Concretely

1 — Defined inputs. Options via Getopt::Long with or die (Chapter 15.2), required ones validated before any work (15.3), environment read with // and an explicit default (15.5). No path appears as a literal. The test: can somebody who did not write it call it correctly from --help alone?

2 — A result another program can read. One record per line, a stable separator, a fixed column order, no decoration. Diagnostics on standard error so the data stays clean — Chapter 15.12 measured a progress message being sorted along with the data when this is ignored.

3 — An exit code that tells the truth. 0 clean, 1 a failing verdict, 2 a usage error, and a distinct code for "could not reach a verdict" — Chapter 15.4 argued that last one must not share with "found violations", and measured exit 256 arriving as 0 and die producing whatever errno happened to be lying around.

4 — Handled failures. Every open with or die "...: $!", close checked on a write, system checked with == 0 and the status decoded in branches (14.3, 15.10). Bad input rows warned about and counted, with the count acted on at the end (14.2).

5 — Output that does not change between identical runs. No localtime, no $$, no rand without a fixed seed, and sort keys %h rather than bare keys — because hash order is randomised per process, which Chapter 6.6 covered and Chapter 15.9's debugger transcript ran into directly.

5. What To Remember

  • Five conditions: declared inputs, a machine-readable result, an honest exit code, handled failures, reproducible output.
  • Measured: three sample scripts scored 5 of 5, 2 of 5 and 0 of 5.
  • A two-script comparison would have proved nothing — a checker I wrote separating two scripts I wrote at the extremes. The middling script is what makes it a measurement.
  • The middling script is good Perl: strict, warnings, command-line input, a checked three-argument open, $! reported. It would pass review.
  • And it fails three conditions, all about being a component rather than a program.
  • It prints prose, so a later step has to parse English and breaks when the wording improves.
  • It has no exit at all, so it exits 0 and reports success with a violation found — the most consequential of the three.
  • It prints localtime, so two identical runs differ and you cannot diff against yesterday.
  • The checker is crude: it cannot tell whether an exit code is honest, only looks for a tab, and says nothing about correctness.
  • All five can be met by a script that computes the wrong answer — the contract makes it usable, Module 27 makes it right.
  • Declared inputs means Getopt::Long with or die, validation first, // defaults, no literal paths.
  • A readable result means one record per line, stable separator, diagnostics on stderr.
  • An honest exit code separates "could not reach a verdict" from "the verdict is fail".
  • Handled failures means every open, close and system checked, bad rows counted, the count acted on.
  • Reproducible output means no localtime, no $$, no unseeded rand, and sort keys.
  • Add a working --help and a documented interface — ten minutes that prevents Chapter 16.2's problem.

That closes Module 16, and with it the bridge into Part II. You now know why the Perl in a flow exists, why it persists for reasons that are not inertia, where you will meet it, which language a given task belongs in on the evidence, which jobs Perl owns in an RTL-to-signoff flow, and what the four misconceptions get right and wrong. Every tool from Module 17 onward is built to this contract, so each one is something you could put in a flow rather than something you read about.

Where this fits

Part of the Perl curriculum.