Perl in EDA flows

Learning objectives
  • Read and maintain the Perl scripts that run older chip-design flows: recognise common legacy idioms
  • Parse options with Getopt::Long
  • Run tools with system using a list of arguments and decode $? into an exit code
  • Capture tool output safely
  • Generate files from here-documents
  • Build a seeded Icarus Verilog regression runner in Perl
  • Decide when a Perl script should be ported to Python
1 Objective→ 2 Learn→ 3 Example→ 4 Practice→ 5 Verify→ 6 Continue
How to study this lessonUnderstand the idea first. Use the example to see the behaviour, practise where provided, then make sure you can explain the result before continuing.

Explanation

Reading legacy Perl

Inherited scripts use idioms that look cryptic until you know them:

Idiom Meaning
while (<>) { ... } Read every line of the files named in @ARGV, or standard input, into $_
chomp; next if /^\s*#/; Strip the newline from $_; skip comment lines
my @list = qw(tb_counter tb_fifo); A list of words without quotes and commas
print "x\n" unless $quiet; unless is if not
local $/; my $text = <$fh>; Read a whole file at once (the line separator is temporarily undefined)
$ENV{PROJ_ROOT}, $0, @ARGV Environment, script name, arguments
my @files = glob("rtl/*.v"); File-name wildcard expansion
__DATA__ at the end of a file Lines after it are readable through the DATA handle
$x =~ tr/a-z/A-Z/; Character-by-character translation

When you take over a script, first add use strict; use warnings;, run perl -c script.pl to check syntax, and fix every reported problem before changing behaviour.

Options with Getopt::Long

use Getopt::Long;
my $seeds = 3;
my $out   = 'build';
my @tests;
GetOptions(
    'seeds=i' => \$seeds,   # =i: an integer is required
    'out=s'   => \$out,     # =s: a string is required
    'test=s'  => \@tests,   # may be given several times
    'verbose' => \my $verbose,
) or usage();

GetOptions removes the options it recognises from @ARGV and returns false for an unknown option or a bad value, so the script can print usage and stop. Options can be written --seeds 4 or --seeds=4.

Running tools and reading $?

system('iverilog', '-g2012', '-o', $vvp, @sources);   # list form: no shell
my $status = $?;

With more than one argument, system runs the program directly. File names containing spaces or characters such as ; are passed exactly and never re-read by a shell. With a single string, system("iverilog $files"), a shell parses the line, which breaks on unusual names and can run unintended commands.

system returns, and stores in $?, a raw wait status rather than the exit code:

Test Meaning
$? == -1 The program could not be started; $! says why
$? & 127 Non-zero: killed by that signal number
$? >> 8 The program's exit code

Capturing output

Backticks, my @lines = `vvp sim.vvp`; or the equivalent qx(vvp sim.vvp), run a command through the shell and return its standard output only; standard error still goes to the terminal. To keep both streams in a log without building a shell string, pass the arguments to sh as "$@":

system('sh', '-c', 'exec "$@" > "$0" 2>&1', $log, @cmd);

Inside sh -c, the first argument after the script becomes $0 (here the log name) and the rest become "$@", so each argument stays exactly as given.

Generating files with here-documents

open(my $fh, '>', "build/$test.f") or die "cannot write build/$test.f: $!\n";
print {$fh} <<"END";
// generated by $0 - do not edit
+incdir+rtl
@{[ join "\n", @sources ]}
tb/$test.v
END
close $fh;

<<"END" interpolates variables like a double-quoted string; <<'END' keeps the text literally, which is what you want for generated Tcl or shell code containing $. @{[ ... ]} inserts the result of an expression.

When to port

Keep a Perl script when it works, is tested and rarely changes. Port it to Python when it needs significant new features, parallel runs, structured JSON or CSV output, or when nobody on the team is comfortable maintaining it. Port by behaviour: record the old script's outputs and exit codes for a set of inputs, then check that the new one matches.

Worked example

The project has rtl/*.v and one testbench per test in tb/, such as tb/tb_fifo.v. A passing testbench prints TEST PASSED; it reads its seed with $value$plusargs("seed=%d", seed). run_regress.pl compiles each test once, runs it with several seeds, keeps a log per run, and exits 0 when everything passed, 1 when anything failed, and 2 for a usage error.

#!/usr/bin/env perl
use strict;
use warnings;
use Getopt::Long;
use File::Path qw(make_path);

sub usage {
    print STDERR "usage: run_regress.pl [--seeds N] [--out DIR] TEST...\n";
    exit 2;
}

# Convert the raw wait status in $? into a shell-style exit code.
sub exit_code {
    my ($status) = @_;
    return 127 if $status == -1;
    return 128 + ($status & 127) if $status & 127;
    return $status >> 8;
}

# Run a command without a shell parsing its arguments; stdout and stderr go to $log.
sub run_logged {
    my ($log, @cmd) = @_;
    system('sh', '-c', 'exec "$@" > "$0" 2>&1', $log, @cmd);
    return exit_code($?);
}

sub log_passed {
    my ($log) = @_;
    open(my $fh, '<', $log) or return 0;
    my ($passed, $error) = (0, 0);
    while (my $line = <$fh>) {
        $passed = 1 if $line =~ /\bTEST PASSED\b/;
        $error  = 1 if $line =~ /\b(?:ERROR|FATAL|TEST FAILED)\b/;
    }
    close $fh;
    return $passed && !$error;
}

my $seeds = 3;
my $out   = 'build/regress';
GetOptions('seeds=i' => \$seeds, 'out=s' => \$out) or usage();
usage() unless @ARGV && $seeds > 0;

my @rtl = glob('rtl/*.v');
my ($runs, @failures) = (0);

for my $test (@ARGV) {
    my $dir = "$out/$test";
    make_path($dir);
    my $vvp = "$dir/sim.vvp";
    my $rc = run_logged("$dir/compile.log",
        'iverilog', '-g2012', '-s', $test, '-o', $vvp, @rtl, "tb/$test.v");
    if ($rc != 0) {
        push @failures, "COMPILE $test (rc=$rc, see $dir/compile.log)";
        next;
    }
    for my $seed (1 .. $seeds) {
        my $log = "$dir/seed$seed.log";
        $rc = run_logged($log, 'vvp', '-n', $vvp, "+seed=$seed");
        $runs++;
        next if $rc == 0 && log_passed($log);
        push @failures, "FAIL    $test seed=$seed (rc=$rc, see $log)";
    }
}

printf "%d runs, %d failures\n", $runs, scalar @failures;
print "$_\n" for @failures;
exit(@failures ? 1 : 0);

Run it:

perl run_regress.pl --seeds 2 tb_counter tb_fifo; echo "status=$?"
perl run_regress.pl --seeds two tb_counter; echo "status=$?"

A compile error is recorded once and the test's seeds are skipped. A run passes only when vvp exits 0 and its log contains TEST PASSED without ERROR, FATAL or TEST FAILED, so a simulation that stopped early cannot pass. --seeds two fails the =i check, so GetOptions returns false and the script exits 2. Each failure line names the log to open.

Check yourself

Why is system('iverilog', @args) safer than system("iverilog @args")? What three cases must you check when decoding $?, and how do you get the exit code? What do backticks capture, and what do they miss? Why does the log name in sh -c 'exec "$@" > "$0" 2>&1' come before the command? What is the difference between <<"END" and <<'END'? What does GetOptions return for an unknown option?

Engineering practice

Practice: Perl in EDA flows

Task→Run / reason→Check result→Explain why

Add a repeatable --define NAME=VALUE option that passes -DNAME=VALUE to iverilog for every test. Write the summary to $out/summary.txt as well as the terminal. Then take a legacy script from your project or this lesson, record its output and exit code for a passing run, a failing run and a missing testbench, and port it to Python in the style of the regression-runner lesson so that all three cases produce the same exit codes.

Practice work is non-certified and is not stored by ASIC Campus.