Skip to content
VLSI Mentor

Perl Part II · Module 28 · Legacy Perl and Python Migration

Choosing a Migration Strategy

Chapter 28.3 showed that rewriting a working script is often the expensive choice. It did not show what to do instead, and the answer depends on the script. There are four sensible strategies: leave the script alone, wrap it behind a cleaner interface, convert it one function at a time, or rewrite it. Each fits a different kind of script. This chapter chooses between them using four facts you can measure: how big the script is, whether it has tests, how often it changes, and how many other programs read what it produces. It measures those facts from a repository's history and its flow tree, applies an explicit rule, shows how close each decision is to changing, and builds the wrapper that one of the scripts turns out to need.

Advanced27 min readPerlPart IIEDA Flows

Part II · Module 28 · Chapter 28.4 · Choosing a Migration Strategy

1. Four Strategies, Defined by What They Change

The four strategies are often talked about loosely. Define each one by what it changes and what it leaves alone, because that is what decides its risk.

StrategyWhat changesWhat staysMain risk
Leave alonenothing; the inventory is written downthe script and all its behaviourthe knowledge decays if nobody records it
Wrapa new, documented interface for new callersthe script, and every existing callertwo interfaces to keep consistent
Incrementalone function at a time, each checked against the old onethe flow, at every stepslow; the half-converted state lasts a while
Rewriteeverything at once, against the inventorynothing, after cutoverevery behaviour difference lands at once

Two things in that table are easy to get wrong.

"Leave alone" is not "do nothing". It means: take the inventory from Chapter 28.2, write down the consumers, and then change nothing until there is a reason. A script left alone with an inventory is one that can be changed safely on the day it must be. A script left alone without one is the problem the next engineer inherits.

"Wrap" does not touch the old script. A wrapper is a small new program that calls the old one and offers a better interface: structured output, a clear exit status, no side effects. Existing callers keep calling the old script directly and see no change. New callers use the wrapper. The old script's behaviour stays frozen, and its circle of consumers stops growing. Section 9 builds one.

2. Four Facts

The canonical criteria are four facts about a script. Each one answers a different question about risk and cost.

  • Size (non-blank, non-comment code lines): how much would have to be rewritten? It sets the cost of a rewrite, and almost nothing else.
  • Tests (does the script have them, or an inventory that serves as one?): could a change be checked at all? Without tests, every strategy except "leave alone" starts by writing them.
  • Change rate (commits that touched the script in the last 12 months): how often does somebody pay the cost of the current code? A script nobody edits costs almost nothing to keep, however ugly it is.
  • Blast radius (how many files in the flow read the script's name or one of its outputs): how many people does a behaviour change reach?

Notice what is missing: how ugly the code is, what language it is in, and line count as a measure of difficulty. Chapter 16.2 measured that a script's interface, not its length, decides how hard it is to replace, and Chapter 16.7 used the same idea to order a migration. Here, size has one narrow job: telling a small rewrite from a big one.

3. The Decision Rule

The rule asks about change rate first, then about either consumers or size:

Figure 28.4.1 — Four facts, one strategy

A two-level decision tree. Left column: Stable, meaning at most two changes a year, and Changing, meaning three or more. Stable leads to Leave alone when fewer than three programs read its output, or to Wrap when three or more do. Changing leads to Rewrite when the script has at most a hundred code lines and tests, or to Incremental conversion otherwise.Stableat most 2 a yearChanging3 or more a yearLeave alonefewer than 3 readersWrap3 or more readersRewritesmall and testedIncrementallarge or untested12
Change rate is asked first, because a script nobody edits is cheap to keep whatever else is true of it. A stable script is left alone, or wrapped if three or more programs read it, so its readers stop multiplying. A changing script is rewritten only if it is small and tested; otherwise it is converted one function at a time, tests first. The thresholds are judgement, set in one place in the decision script.

Why change rate comes first. If a script is not changing, the cost of its ugliness is not being paid by anyone, so neither its size nor its lack of tests creates any pressure to act. The only question left is whether its circle of readers is growing; if it is, wrap it. Asking about size first gives a different and worse answer, as section 8 measures.

Why "small and tested" for a rewrite. A rewrite replaces every behaviour at once, so it is only safe when the inventory is short (small) and checkable (tested). Everything else that changes often is converted incrementally: one function at a time, each one compared with the original, which is Chapter 28.5's subject.

4. Measuring the Facts

Opinions about these four facts are usually wrong in a predictable direction: the script someone dislikes is "constantly changing", and the one nobody looks at "has no users". Measure them instead. To keep this page runnable anywhere, make_repo.sh builds a throwaway git repository holding five sample flow scripts, three test files, eight consumer files and a backdated history: 24 commits, with dates set explicitly so that "the last 12 months" means the same thing on every machine. Only sta_summary.pl is a real script (the case study); the other four are padded with straight-line filler to the sizes being tested.

Azvya Education Pvt. Ltd.VLSI Mentor
make_repo.sh
#!/bin/bash
# make_repo.sh - build a throwaway git repository holding five SAMPLE flow
# scripts, their tests, their consumers and a backdated history, so that
# every fact the strategy needs can be MEASURED rather than asserted.
# The scripts are padded samples; only their sizes, tests, histories and
# consumers matter here.
set -e
repo=$1
mkdir -p "$repo"/{tools,t,flow/bin}
cp sta_summary.pl "$repo/tools/"
cd "$repo"
git init -q
git config user.name  "sample"
git config user.email "sample@example.invalid"
git config commit.gpgsign false

pad() {      # pad FILE N : append N lines of plausible straight-line code
    for i in $(seq 1 "$2"); do echo "\$cfg{opt_$i} = $i;" >> "$1"; done
}
printf '#!/usr/bin/perl\nuse strict; use warnings;\nmy %%cfg;\nwhile (<>) { chomp; printf "%%-24s %%8s\\n", split /\\s+/ }\n' > tools/fmt_table.pl
printf '#!/usr/bin/perl\nuse strict; use warnings;\nmy %%cfg;\nopen my $out, ">", "rtl.f" or die;\n' > tools/gen_filelist.pl
pad tools/gen_filelist.pl 56
printf '#!/usr/bin/perl\nmy %%cfg;\nopen(LOG, ">regress.log");\nsystem("bsub -q normal sim");\n' > tools/run_regress.pl
pad tools/run_regress.pl 410
printf '#!/usr/bin/perl\nuse strict; use warnings;\nmy %%cfg;\nopen my $out, ">", "coverage.csv" or die;\n' > tools/parse_cov.pl
pad tools/parse_cov.pl 226
for s in fmt_table gen_filelist parse_cov; do
    printf 'use Test::More;\nok(1, "%s");\ndone_testing;\n' "$s" > "t/$s.t"
done

# consumers: files in the flow that name a script, or a file it writes
printf 'rtl.f:\n\tperl tools/gen_filelist.pl\nsignoff:\n\tperl tools/sta_summary.pl reports\nregress:\n\tperl tools/run_regress.pl\n' > flow/Makefile
echo 'cut -d, -f3 run/tmp.csv'                    > flow/bin/post_wns.sh
echo 'perl ../tools/sta_summary.pl -v run/reports' > flow/bin/weekly_digest.pl
echo 'grep FAIL regress.log | mail -s fails team'  > flow/bin/mail_fails.sh
echo 'tail -1 regress.log'                         > flow/bin/status.sh
echo 'perl tools/run_regress.pl --rerun'           > flow/bin/rerun.sh
echo 'column -t coverage.csv'                      > flow/bin/cov_view.sh
echo 'perl tools/fmt_table.pl < slack.txt'         > flow/bin/pretty.sh

commit() {   # commit DATE FILE... : one backdated commit touching FILEs
    local d=$1; shift
    for f in "$@"; do echo "# touched $d" >> "$f"; done
    git add -A
    GIT_AUTHOR_DATE="$d 10:00:00" GIT_COMMITTER_DATE="$d 10:00:00" \
        git commit -q -m "change $* on $d"
}
commit 2016-03-01 tools/sta_summary.pl tools/fmt_table.pl tools/run_regress.pl tools/gen_filelist.pl tools/parse_cov.pl
commit 2025-12-02 tools/fmt_table.pl
for d in 2025-11-04 2025-12-09 2026-01-13 2026-02-10 2026-03-10 2026-04-14 2026-05-12 2026-06-09 2026-07-14 2026-08-11 2026-09-08 2026-09-29; do
    commit "$d" tools/run_regress.pl
done
for d in 2026-01-20 2026-03-17 2026-05-19 2026-07-21 2026-09-15; do
    commit "$d" tools/gen_filelist.pl
done
for d in 2025-11-18 2026-02-24 2026-04-28 2026-06-23 2026-08-25; do
    commit "$d" tools/parse_cov.pl
done

measure_scripts.pl then measures each fact directly from the repository:

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

# measure_scripts.pl - the four facts a migration strategy needs, MEASURED.
# usage: perl measure_scripts.pl REPO SINCE UNTIL
# Prints one tab-separated row per tools/*.pl:
#   script  code_lines  tested  changes  consumers  (consumer files)
my ($repo, $since, $until) = @ARGV;
die "usage: measure_scripts.pl REPO SINCE UNTIL\n" unless defined $until;

my @flow;                                            # every file in the flow
find({ no_chdir => 1, wanted => sub { push @flow, $_ if -f } }, "$repo/flow");

print join("\t", qw(script code_lines tested changes consumers)), "\n";
for my $path (sort glob "$repo/tools/*.pl") {
    my $name = basename($path, '.pl');
    open my $fh, '<', $path or die "cannot read $path: $!\n";
    my ($code, @outputs) = (0);
    while (my $line = <$fh>) {
        next if $line =~ /^\s*(?:#|$)/;              # blank or comment
        $code++;
        # a file it writes: two-argument ">name" or three-argument ">", "name"
        push @outputs, $1 if $line =~ /\bopen\b[^;]*?>+\s*(?:["']\s*,\s*["'])?([\w.\/]+)/;
    }
    close $fh;

    my $tested = -e "$repo/t/$name.t" ? 'yes' : 'no';

    # change rate: commits touching the file inside the window
    my @log = `git -C "$repo" log --oneline --since="$since" --until="$until" -- "tools/$name.pl"`;
    die "git log failed for $name\n" if $?;

    # blast radius: flow files that name the script OR a file it writes
    my $pattern = join '|', map { quotemeta } $name, @outputs;
    my @readers;
    for my $f (@flow) {
        open my $in, '<', $f or next;
        local $/;
        push @readers, basename($f) if <$in> =~ /$pattern/;
    }
    print join("\t", "$name.pl", $code, $tested, scalar @log, scalar @readers,
                     join(',', sort @readers)), "\n";
}
  • Code lines skip blank lines and comments. POD and here-documents would need Chapter 28.1's survey logic; these samples have neither.
  • Tested means a test file named after the script exists. That is a crude proxy, and in your own repository you should replace it with whatever your team actually runs, or with "an inventory exists".
  • Changes come from git log --since --until -- FILE, counted one line per commit. The window is given explicitly rather than as "12 months ago", so the answer does not drift with the date you run it.
  • Consumers are flow files that mention the script's name or any file it writes. The output files are found from the script's own open statements: the same idea as the OUTPUTS section of Chapter 28.1's survey.

5. The Measurement Had a Bug

The first version of measure_scripts.pl found a script's output files with this pattern:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
push @outputs, $1 if $line =~ /\bopen\b.*?>\s*"?([\w.]+)/;

It handles the two-argument form open(CSV, ">tmp.csv"). It does not handle the three-argument form open my $out, ">", "coverage.csv", where a quote and a comma sit between the > and the name. With that version, parse_cov.pl was reported with zero consumers, because nothing found coverage.csv, so nothing found cov_view.sh, the program that reads it.

That one missing match would have made parse_cov.pl look like the safest script in the tree to change. The fixed pattern accepts both forms and finds the consumer. The lesson is the same as the 996-line __END__ in Chapter 28.1: a tool that measures risk can understate it, and the direction of the error matters. Undercounting consumers is the dangerous direction, so check any "zero" against a quick search by hand.

6. The Decisions

choose_strategy.pl applies the rule. The thresholds are named constants at the top, with a comment that says they are judgement:

Azvya Education Pvt. Ltd.VLSI Mentor
choose_strategy.pl
use strict;
use warnings;

# choose_strategy.pl - turn four measured facts into a migration strategy.
# usage: perl choose_strategy.pl [--size-first] facts.tsv
#
# The THRESHOLDS are judgement, not measurement. They are named constants so
# that a team can argue about them in one place; section 6 shows how far
# each decision is from changing.
use constant {
    STABLE_MAX   => 2,     # at most this many changes in 12 months = stable
    MANY_READERS => 3,     # this many consumer files or more = load-bearing
    SMALL_MAX    => 100,   # at most this many code lines = small
};
my $size_first = @ARGV && $ARGV[0] eq '--size-first' ? shift : 0;

sub decide {
    my ($f) = @_;
    my $stable = $f->{changes} <= STABLE_MAX;
    my $small_and_tested = $f->{code_lines} <= SMALL_MAX && $f->{tested} eq 'yes';
    return 'REWRITE' if $size_first && $small_and_tested;    # the WRONG order
    if ($stable) {
        return $f->{consumers} >= MANY_READERS ? 'WRAP' : 'LEAVE ALONE';
    }
    return $small_and_tested ? 'REWRITE' : 'INCREMENTAL';
}
sub first_step {
    my ($strategy, $f) = @_;
    return $strategy eq 'INCREMENTAL' && $f->{tested} eq 'no'
         ? 'write tests first'
         : { 'LEAVE ALONE' => 'inventory it, change nothing',
             'WRAP'        => 'inventory it, give new readers a wrapper',
             'REWRITE'     => 'inventory it, rewrite against the inventory',
             'INCREMENTAL' => 'convert one function at a time' }->{$strategy};
}

# what single fact, moved just across its threshold, would change the answer?
my @nudge = (
    [ changes    => sub { $_[0] <= STABLE_MAX ? STABLE_MAX + 1 : STABLE_MAX } ],
    [ consumers  => sub { $_[0] >= MANY_READERS ? MANY_READERS - 1 : MANY_READERS } ],
    [ code_lines => sub { $_[0] <= SMALL_MAX ? SMALL_MAX + 1 : SMALL_MAX } ],
    [ tested     => sub { $_[0] eq 'yes' ? 'no' : 'yes' } ],
);

my ($header, @rows) = <>;
chomp $header;
my @col = split /\t/, $header;
my (@decided, @margin);
for my $row (@rows) {
    chomp $row;
    my %f;
    @f{@col} = split /\t/, $row;
    my $s = decide(\%f);
    push @decided, sprintf "%-16s %-12s %s\n", $f{script}, $s, first_step($s, \%f);
    for my $n (@nudge) {
        my ($fact, $move) = @$n;
        my %g = (%f, $fact => $move->($f{$fact}));
        my $t = decide(\%g);
        push @margin, sprintf "%-16s %-12s %-20s %s\n", $f{script}, $s,
            "$fact $f{$fact}->$g{$fact}", $t if $t ne $s;
    }
}
printf "%-16s %-12s %s\n", 'SCRIPT', 'STRATEGY', 'FIRST STEP';
print @decided;
printf "\n%-16s %-12s %-20s %s\n", 'SCRIPT', 'STRATEGY', 'WOULD CHANGE IF', 'TO';
print @margin;

The case-study script, with its two reports, is part of the repository and of the wrapper in section 9:

Azvya Education Pvt. Ltd.VLSI Mentor
sta_summary.pl
#!/usr/bin/perl
# sta_summary.pl - summarise per-corner timing reports
# usage: sta_summary.pl [-v] [report_dir]
#   prints one line per corner: paths, violations, WNS and TNS
#
# (A SAMPLE legacy script written for this module, in the style of
#  scripts that have run unchanged in a flow for a decade.)

use Getopt::Long;

$verbose = 0;
GetOptions("v" => \$verbose);

$dir = shift @ARGV;
$dir = "." unless $dir;

opendir(D, $dir) || die "cannot open $dir: $!\n";
@files = grep(/\.rpt$/, readdir(D));
closedir(D);

if (!@files) {
    print "No reports found in $dir\n";
    exit 0;
}

foreach $f (@files) {
    ($corner = $f) =~ s/\.rpt$//;
    open(F, "$dir/$f") || next;
    while (<F>) {
        if (/^Endpoint\s*:\s*(\S+)/) {
            $ep = $1;
        }
        if (/slack \((\w+)\)\s+(\S+)/) {
            $paths{$corner}++;
            if ($1 eq "VIOLATED") {
                $viol{$corner}++;
                $tns{$corner} += $2;
                push(@{$eps{$corner}}, $ep);
            }
            $wns{$corner} = $2 if $2 < $wns{$corner};
        }
    }
    close(F);
}

open(CSV, ">tmp.csv");
$bad = 0;
foreach $c (sort keys %paths) {
    printf("%-16s paths=%-3d viol=%-3d wns=%.3f tns=%.3f\n",
           $c, $paths{$c}, $viol{$c}, $wns{$c}, $tns{$c});
    print CSV "$c,$viol{$c},$wns{$c}\n";
    if ($verbose) {
        foreach $e (@{$eps{$c}}) { print "    $e\n"; }
    }
    $bad++ if $viol{$c};
}
close(CSV);

exit $bad;
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/r_reg[1]
Endpoint  : u_core/acc_reg[2]
  slack (VIOLATED)   -0.011

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.087

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

End of report
Azvya Education Pvt. Ltd.VLSI Mentor
run_strategy.sh
#!/bin/bash
# Measure the five sample scripts, then choose a strategy for each.
repo=$(mktemp -d)/flowrepo
bash make_repo.sh "$repo"
echo "--- measured facts, 12 months to 2026-10-13"
perl measure_scripts.pl "$repo" 2025-10-13 2026-10-13 > facts.tsv
column -t -s $'\t' facts.tsv
echo
perl choose_strategy.pl facts.tsv
echo
echo "--- the same facts with the size rule checked FIRST"
diff <(perl choose_strategy.pl facts.tsv | sed -n '2,6p') \
     <(perl choose_strategy.pl --size-first facts.tsv | sed -n '2,6p')
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
--- measured facts, 12 months to 2026-10-13
script           code_lines  tested  changes  consumers
fmt_table.pl     3           yes     1        1          pretty.sh
gen_filelist.pl  59          yes     5        1          Makefile
parse_cov.pl     229         yes     5        1          cov_view.sh
run_regress.pl   413         no      12       4          Makefile,mail_fails.sh,rerun.sh,status.sh
sta_summary.pl   44          no      0        3          Makefile,post_wns.sh,weekly_digest.pl

SCRIPT           STRATEGY     FIRST STEP
fmt_table.pl     LEAVE ALONE  inventory it, change nothing
gen_filelist.pl  REWRITE      inventory it, rewrite against the inventory
parse_cov.pl     INCREMENTAL  convert one function at a time
run_regress.pl   INCREMENTAL  write tests first
sta_summary.pl   WRAP         inventory it, give new readers a wrapper

SCRIPT           STRATEGY     WOULD CHANGE IF      TO
fmt_table.pl     LEAVE ALONE  changes 1->3         REWRITE
fmt_table.pl     LEAVE ALONE  consumers 1->3       WRAP
gen_filelist.pl  REWRITE      changes 5->2         LEAVE ALONE
gen_filelist.pl  REWRITE      code_lines 59->101   INCREMENTAL
gen_filelist.pl  REWRITE      tested yes->no       INCREMENTAL
parse_cov.pl     INCREMENTAL  changes 5->2         LEAVE ALONE
parse_cov.pl     INCREMENTAL  code_lines 229->100  REWRITE
run_regress.pl   INCREMENTAL  changes 12->2        WRAP
sta_summary.pl   WRAP         changes 0->3         INCREMENTAL
sta_summary.pl   WRAP         consumers 3->2       LEAVE ALONE

--- the same facts with the size rule checked FIRST
1c1
< fmt_table.pl     LEAVE ALONE  inventory it, change nothing
---
> fmt_table.pl     REWRITE      inventory it, rewrite against the inventory

Read the five decisions as five different situations.

  • fmt_table.pl: LEAVE ALONE. Tiny and tested, so a rewrite would be easy, but it changed once in a year and one program reads it. There is nothing to gain. Easy to rewrite is not the same as worth rewriting.
  • gen_filelist.pl: REWRITE. It changes about every two months, it is 59 lines, and it has tests. This is the case Chapter 28.3 described where a rewrite is right: the change is coming anyway, and the inventory is small enough to check.
  • parse_cov.pl: INCREMENTAL. Tested and actively changing, but 229 lines. Converting it one function at a time keeps every intermediate state working.
  • run_regress.pl: INCREMENTAL, tests first. Twelve changes a year, four consumers, 413 lines and no tests. This is the regression driver of Chapter 16.7, and the first step is not converting anything. It is building the test harness that makes conversion checkable.
  • sta_summary.pl: WRAP. It has not changed in the window at all, but three programs read its outputs. Leave its behaviour exactly as it is, and stop the circle of readers growing by giving new callers a wrapper.

7. How Far Each Decision Is From Changing

A decision that would flip if one fact moved a little is a weak decision. The second table in the output checks this. For each script it moves each fact just across its threshold and reports any move that changes the answer.

Two of the rows matter most.

sta_summary.pl is one consumer away from LEAVE ALONE (consumers 3->2). If weekly_digest.pl were retired, the recommendation would be to leave the script entirely alone. That is worth knowing before spending a week on a wrapper: it may be cheaper to remove a consumer than to wrap the producer.

run_regress.pl changes only if its change rate falls from 12 to 2. No size or test fact flips it, because it is large and untested and changing constantly. That is a robust decision, and the one to defend most firmly when someone proposes "just leaving the regression driver alone".

Two rows are close for a different reason. fmt_table.pl would become a REWRITE at three changes a year, and gen_filelist.pl a LEAVE ALONE at two. Those are scripts near the stable/changing line. For them, the right question is not "which strategy?" but "is that change rate going to continue?", and the git log alone cannot answer it.

8. Rule Order Matters

The last part of run_strategy.sh runs the same facts through a version of the rule that checks "small and tested" first, before change rate, and shows which decisions change. One does. fmt_table.pl goes from LEAVE ALONE to REWRITE.

That is the mistake behind many unnecessary rewrites. "It is small and it has tests, so let's rewrite it" sounds responsible. But the script changed once in a year and has one reader, so the rewrite buys nothing and still carries every risk of Chapter 28.3. Asking "does anyone pay for this script's current shape?" first is what prevents it.

9. What Wrap Looks Like on Day One

sta_summary.pl came out as WRAP. Here is that strategy as code: a Python wrapper that calls the legacy script, unchanged, and offers new consumers a documented interface. It returns JSON with an explicit schema (the hand-off discipline of Chapter 26.3), a three-valued exit status, and no side effects in the caller's directory.

Azvya Education Pvt. Ltd.VLSI Mentor
sta_summary_json.py
#!/usr/bin/env python3
"""sta_summary_json.py - a documented interface IN FRONT OF sta_summary.pl.

The legacy script is not changed, and its existing consumers keep calling it
directly. NEW consumers call this wrapper instead and get:

  STDOUT  one JSON object: {"schema": 1, "corners": [...]}
          wns is null when the legacy script did not record one
  exit    0 every corner met timing
          1 at least one corner violated
          2 no summary could be produced (missing or empty directory)

The legacy script runs in a private scratch directory, so its tmp.csv side
effect can never land in the caller's working directory.
"""
import json
import os
import re
import subprocess
import sys
import tempfile

LEGACY = os.path.join(os.path.dirname(os.path.abspath(__file__)), "sta_summary.pl")
LINE = re.compile(r"^(\S+)\s+paths=(\d+)\s+viol=(\d+)\s+wns=\S+\s+tns=(\S+)$")


def main():
    report_dir = os.path.abspath(sys.argv[1] if len(sys.argv) > 1 else ".")
    with tempfile.TemporaryDirectory() as scratch:
        run = subprocess.run(["perl", LEGACY, report_dir], cwd=scratch,
                             capture_output=True, text=True)
        csv_path = os.path.join(scratch, "tmp.csv")
        csv = {}
        if os.path.exists(csv_path):              # absent = no summary was made
            with open(csv_path) as fh:
                for row in fh.read().splitlines():
                    corner, viol, wns = row.split(",")
                    csv[corner] = wns

    # The legacy exit status is ambiguous: 2 means "two corners violated" OR
    # "could not open the directory" (errno 2). Decide from the files instead.
    if not csv:
        msg = run.stderr.strip() or run.stdout.strip() or "no summary produced"
        print(f"sta_summary_json: {msg}", file=sys.stderr)
        return 2

    corners = []
    for line in run.stdout.splitlines():
        m = LINE.match(line)
        if m:
            name, paths, viol, tns = m.groups()
            wns = csv.get(name, "")
            corners.append({"corner": name, "paths": int(paths),
                            "violations": int(viol),
                            "wns": float(wns) if wns else None,
                            "tns": float(tns)})
    print(json.dumps({"schema": 1, "corners": corners}, indent=1))
    return 1 if any(c["violations"] for c in corners) else 0


if __name__ == "__main__":
    sys.exit(main())

Three design decisions in it come straight from the inventory of Chapter 28.2.

It runs the legacy script in a private scratch directory. That contains B5: the tmp.csv side effect lands in the wrapper's temporary directory and is read from there. It can never overwrite a file in the caller's directory, and it can never be yesterday's file.

It reports wns as null when the legacy script did not record one. B4 means a clean corner's real worst slack is unknown to the old script. The wrapper does not invent 0.0 for it. It says "not recorded", which is the truth, and which a JSON consumer can test for.

It does not trust the legacy exit status. exit $bad is a count, and a missing directory exits with errno 2, so exit 2 means either "two corners violated" or "the directory does not exist". The wrapper cannot tell those apart from the number. It decides from the files instead: no tmp.csv means no summary was produced.

Azvya Education Pvt. Ltd.VLSI Mentor
wrap_demo.sh
#!/bin/bash
# The wrapper against the legacy script, side by side, in a scratch caller dir.
here=$(pwd)
work=$(mktemp -d)
cp -R reports "$work"/
cd "$work" || exit 1

echo "=== new consumer: the wrapper"
python3 "$here/sta_summary_json.py" reports; echo "exit=$?"
echo "tmp.csv in the caller's directory: $([ -e tmp.csv ] && echo yes || echo no)"

echo "=== old consumer: the legacy script, unchanged"
perl "$here/sta_summary.pl" reports > /dev/null; echo "exit=$?"
echo "tmp.csv in the caller's directory: $([ -e tmp.csv ] && echo yes || echo no)"

echo "=== the ambiguous status: two violating corners vs a missing directory"
mkdir two && cp reports/ss_0p72v_125c.rpt two/a.rpt && cp reports/ss_0p72v_125c.rpt two/b.rpt
for d in two nosuch; do
    perl "$here/sta_summary.pl" "$d" > /dev/null 2>&1; legacy=$?
    python3 "$here/sta_summary_json.py" "$d" > /dev/null 2>&1; wrapped=$?
    echo "  $d: legacy exit $legacy, wrapper exit $wrapped"
done

echo "=== 256 failing corners"
mkdir many
for i in $(seq -w 1 256); do cp reports/ss_0p72v_125c.rpt many/c$i.rpt; done
perl "$here/sta_summary.pl" many > /dev/null; echo "  legacy exit $?"
python3 "$here/sta_summary_json.py" many > /dev/null; echo "  wrapper exit $?"
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
=== new consumer: the wrapper
{
 "schema": 1,
 "corners": [
  {
   "corner": "ss_0p72v_125c",
   "paths": 3,
   "violations": 2,
   "wns": -0.043,
   "tns": -0.054
  },
  {
   "corner": "tt_0p80v_25c",
   "paths": 2,
   "violations": 0,
   "wns": null,
   "tns": 0.0
  }
 ]
}
exit=1
tmp.csv in the caller's directory: no
=== old consumer: the legacy script, unchanged
exit=1
tmp.csv in the caller's directory: yes
=== the ambiguous status: two violating corners vs a missing directory
  two: legacy exit 2, wrapper exit 1
  nosuch: legacy exit 2, wrapper exit 2
=== 256 failing corners
  legacy exit 0
  wrapper exit 1

The demonstration shows the point of the strategy. New consumers get structured data, an honest null, a clean working directory, and an exit status that means one thing: 1 for two violating corners, 2 for a missing directory, and 1 (not 0) for 256 failing corners. Old consumers get exactly what they had before, tmp.csv included, because nothing they call has changed. The legacy behaviour is frozen, and every new reader is added to the wrapper instead of to the script's blast radius.

The cost is real, and it belongs in the decision. There are now two interfaces, and the wrapper depends on the legacy STDOUT format (its LINE pattern). That dependency is exactly why the inventory matters: B1 is now load-bearing for the wrapper too, and any later change to the old script must be checked against both.

10. Mistakes When Choosing

Choosing by how much you dislike the code. Ugliness is not one of the four facts. sta_summary.pl is the ugliest script in the repository and the decision was to keep its behaviour exactly.

Estimating instead of measuring. "It changes all the time" and "nobody uses that" are both checkable in a minute with git log and a search. The measurement in section 4 took less time than the argument usually does.

Trusting a zero. The first measurement reported zero consumers for a script that had one. Check the "safe" readings by hand.

Asking about size first. It turned one LEAVE ALONE into an unnecessary REWRITE.

Ignoring the margin. A decision one consumer away from changing deserves a different conversation from one that would need ten fewer commits a year.

Treating the strategy as permanent. The facts change. A wrapped script that starts changing every month crosses into INCREMENTAL. Re-run the measurement when you plan each quarter.

11. Exercises

1. Your own five. Point measure_scripts.pl at a real repository (adjust the tools/, flow/ and t/ paths) and run choose_strategy.pl on the result. Which decision surprised you, and which fact caused it? Check that fact by hand.

2. Retire a consumer. In the sample repository, delete flow/bin/weekly_digest.pl and re-run. Confirm that sta_summary.pl moves to LEAVE ALONE. Then argue whether retiring the digest or writing the wrapper is the cheaper action, and what you would need to know to decide.

3. A fifth strategy? Some teams add "delete it": the script's output has no remaining consumers. Add a rule for it to choose_strategy.pl. Where in the order must it go, and what fact would you need that the four do not give you?

4. Break the wrapper. Change the legacy script's printf format by one space and run wrap_demo.sh again. What does the wrapper report, and what should it report? Change the wrapper so that a format it does not recognise produces exit 2 and a message, instead of a JSON object with fewer corners.

5. Weighting the facts. Replace the rule with a weighted score (like Chapter 16.7's) and compare its ranking with the rule's decisions for the five scripts. Which do you trust more, and why is a rule with explicit thresholds easier to argue about than a score?

12. What To Remember

  • Four strategies: leave alone, wrap, incremental, rewrite. Define each by what it changes and what it leaves alone.
  • Leave alone still means taking the inventory. Wrap means a new interface for new callers, with the old script untouched.
  • Four facts decide: size, tests, change rate and blast radius, all measurable.
  • Change rate is asked first. A script nobody edits is cheap to keep whatever it looks like.
  • Stable and read by many → wrap. Changing, small and tested → rewrite. Changing otherwise → incremental, tests first if there are none.
  • The thresholds are judgement; keep them as named constants in one place.
  • Measure the facts from git and the flow tree. Here they took one script and a few seconds.
  • Measured: the first version undercounted consumers by missing three-argument open, and made a risky script look safe.
  • Look at the margin: sta_summary.pl was one consumer from LEAVE ALONE, and run_regress.pl would need its change rate to fall from 12 to 2.
  • Measured: asking about size first turned 1 of 5 decisions into an unnecessary rewrite.
  • A wrapper contains side effects, refuses to invent values, and resolves ambiguous exit statuses, while every old caller sees no change.
  • The facts change, so re-measure when planning, not once.

Chapter 28.5 takes the strategy that two of the five scripts landed on, incremental conversion, and does it: one function at a time, with the Perl and Python versions running side by side against the same input until they agree.

Where this fits

Part of the Perl curriculum.