Skip to content
VLSI Mentor

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

Perl Regex Features Python Does Not Have

Chapter 16.8 compared the two regular-expression engines and found them mostly the same. Porting a Perl script still takes real work around its patterns, because in Perl a pattern is part of the language and in Python it is a library. Perl can compute a replacement inline, transliterate characters with an operator that is not a regex at all, scan a string token by token from where the last match stopped, collect named captures into a hash, and match against a variable nobody wrote down. This chapter takes each of those constructs, measures a literal Python port against the Perl original, and gives the rewrite that behaves the same.

Advanced25 min readPerlPart IIEDA Flows

Part II · Module 28 · Chapter 28.7 · Perl Regex Features Python Does Not Have

1. The Engine Is Not the Problem

Chapter 16.8 ran sixteen regex features through both engines and found eleven identical, four Perl-only (variable-length lookbehind, \p{...}, (?R) and \K) and one available in neither. If you are porting a pattern, its conclusion still holds: the pattern itself almost always moves across unchanged, or with a spelling change such as (?<name> to (?P<name>.

The porting work is in the operators. Perl gives a pattern a set of operators that Python provides only as library functions, if at all:

  • s///e, which runs code to build each replacement;
  • tr///, which is not a regex and only looks like one;
  • \G with /gc, which continues from where the last match stopped and refuses to skip;
  • %+, a hash of named captures;
  • the implicit $_, and $1 that survives a failed match.

Each section below runs a Perl original and a Python port side by side, in a setting from flow scripts: constraint files, hierarchical names, netlist lines, timing reports and report headers.

2. /e: Computing the Replacement

With /e, the replacement part of s/// is Perl code, run once per match, and its value is what gets inserted (Chapter 11.14). Here it converts nanosecond values in constraint lines to picoseconds:

Azvya Education Pvt. Ltd.VLSI Mentor
rx/eval_subst.pl
use strict;
use warnings;

# convert nanosecond values in constraint lines to picoseconds
my @sdc = ("set_input_delay 0.5ns -clock clk [get_ports din]",
           "set_output_delay 1.25ns -clock clk [get_ports dout]",
           "set_max_delay 0.07ns -from u_a -to u_b");
for my $line (@sdc) {
    (my $ps = $line) =~ s/(\d+(?:\.\d+)?)ns\b/($1 * 1000) . "ps"/ge;
    print "$ps\n";
}
Azvya Education Pvt. Ltd.VLSI Mentor
rx/eval_subst.py
import re

sdc = ["set_input_delay 0.5ns -clock clk [get_ports din]",
       "set_output_delay 1.25ns -clock clk [get_ports dout]",
       "set_max_delay 0.07ns -from u_a -to u_b"]
PAT = r"(\d+(?:\.\d+)?)ns\b"
print("-- a replacement STRING cannot compute:")
print(re.sub(PAT, r"\1*1000ps", sdc[0]))
print("-- a FUNCTION can, but it prints Python's float:")
for line in sdc:
    print(re.sub(PAT, lambda m: f"{float(m.group(1)) * 1000}ps", line))
print("-- a function that prints the number the way Perl does:")
for line in sdc:
    print(re.sub(PAT, lambda m: "%.15gps" % (float(m.group(1)) * 1000), line))
Azvya Education Pvt. Ltd.VLSI Mentor
rx_eval_subst.sh
#!/bin/bash
echo "--- perl rx/eval_subst.pl"
perl rx/eval_subst.pl
echo "--- python3 rx/eval_subst.py"
python3 rx/eval_subst.py
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
--- perl rx/eval_subst.pl
set_input_delay 500ps -clock clk [get_ports din]
set_output_delay 1250ps -clock clk [get_ports dout]
set_max_delay 70ps -from u_a -to u_b
--- python3 rx/eval_subst.py
-- a replacement STRING cannot compute:
set_input_delay 0.5*1000ps -clock clk [get_ports din]
-- a FUNCTION can, but it prints Python's float:
set_input_delay 500.0ps -clock clk [get_ports din]
set_output_delay 1250.0ps -clock clk [get_ports dout]
set_max_delay 70.0ps -from u_a -to u_b
-- a function that prints the number the way Perl does:
set_input_delay 500ps -clock clk [get_ports din]
set_output_delay 1250ps -clock clk [get_ports dout]
set_max_delay 70ps -from u_a -to u_b

Three ports, three results:

  1. A replacement string (r"\1*1000ps") cannot compute anything. Python inserts the captured text and the literal characters *1000, giving 0.5*1000ps: a constraint line no tool would accept, and no error from Python.
  2. A replacement function is the real equivalent of /e: re.sub calls it with the match object for every match. But it returns Python's float formatting, 500.0ps where Perl wrote 500ps. A timing tool may accept both. A diff against the Perl output will not.
  3. A function that formats like Perl, with "%.15g" (the rule from Chapter 28.5), reproduces the original exactly, including 70ps for 0.07ns.

The rule: /e becomes a function, and the function must format its result the way the Perl expression stringified it.

3. tr///: Not a Regex at All

tr/// (Chapter 11.15) maps characters one for one. Its two halves are lists of characters, not patterns. And it returns a count, which is often the only reason it is there:

Azvya Education Pvt. Ltd.VLSI Mentor
rx/translit.pl
use strict;
use warnings;

my $path = "u_core/u_alu/acc_reg[7]";
my $depth = ($path =~ tr{/}{});                 # tr returns a COUNT; empty list = count only
(my $dotted = $path) =~ tr{/}{.};               # character-for-character mapping
(my $safe = $path) =~ tr/a-zA-Z0-9_//cd;        # c = complement, d = delete
(my $squeezed = "u_core//u_alu///acc") =~ tr{/}{}s;  # s = squeeze repeats
print "depth=$depth\ndotted=$dotted\nsafe=$safe\nsqueezed=$squeezed\n";
(my $dots = "ss_0.72v.lib") =~ tr/./_/;         # tr is NOT a regex: "." is just a dot
print "dots=$dots\n";
Azvya Education Pvt. Ltd.VLSI Mentor
rx/translit.py
import re

path = "u_core/u_alu/acc_reg[7]"
print(f"depth={path.count('/')}")
print(f"dotted={path.translate(str.maketrans('/', '.'))}")
print(f"safe={re.sub(r'[^a-zA-Z0-9_]', '', path)}")
print(f"squeezed={re.sub(r'/+', '/', 'u_core//u_alu///acc')}")
# tr/./_/ translated as a regex substitution, character for character:
print(f"dots={re.sub('.', '_', 'ss_0.72v.lib')}")
Azvya Education Pvt. Ltd.VLSI Mentor
rx_translit.sh
#!/bin/bash
echo "--- perl rx/translit.pl"
perl rx/translit.pl
echo "--- python3 rx/translit.py"
python3 rx/translit.py
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
--- perl rx/translit.pl
depth=2
dotted=u_core.u_alu.acc_reg[7]
safe=u_coreu_aluacc_reg7
squeezed=u_core/u_alu/acc
dots=ss_0_72v_lib
--- python3 rx/translit.py
depth=2
dotted=u_core.u_alu.acc_reg[7]
safe=u_coreu_aluacc_reg7
squeezed=u_core/u_alu/acc
dots=____________

The first four results match, and each needs a different Python tool:

PerlWhat it doesPython
$n = ($s =~ tr{/}{})counts characters (hierarchy depth here)s.count("/")
tr{/}{.}maps one character to anothers.translate(str.maketrans("/", "."))
tr/a-zA-Z0-9_//cddeletes everything not in the listre.sub(r"[^a-zA-Z0-9_]", "", s)
tr{/}{}ssqueezes runs of a characterre.sub(r"/+", "/", s)

The last line of the output is the trap. In tr/./_/ the dot is a dot: Perl turned ss_0.72v.lib into ss_0_72v_lib. A translator who reads it as a substitution writes re.sub('.', '_', ...), where the dot is any character, and the library name becomes twelve underscores. The fix is to stop treating tr as a regex at all: use str.translate for mappings, and if a regex is genuinely needed, escape the characters (re.escape) or put them in a character class.

4. \G: A Lexer That Stops, and One That Skips

\G anchors a match at the position where the previous /g match on the same string ended, and the /c flag keeps that position when a match fails (Chapter 11.20). Together they make a lexer: try each token pattern at the current position, and if none fits, stop and report the column. Here it tokenises a gate-level instance, and the second line has a stray #, the kind of corruption a broken netlist writer produces:

Azvya Education Pvt. Ltd.VLSI Mentor
rx/lexer.pl
use strict;
use warnings;

# tokenise one gate-level instance; the second line has a stray '#'
for my $s ("AND2X1 u_and (.A(n1), .B(n2), .Y(n3));",
           "AND2X1 u_and (.A(n1), #.B(n2), .Y(n3));") {
    my @tok;
    pos($s) = 0;
    while (pos($s) < length $s) {
        if    ($s =~ /\G\s+/gc)          { }                       # skip spaces
        elsif ($s =~ /\G(\.\w+)/gc)      { push @tok, "PIN:$1" }
        elsif ($s =~ /\G(\w+)/gc)        { push @tok, "ID:$1" }
        elsif ($s =~ /\G([(),;])/gc)     { push @tok, $1 }
        else  { push @tok, "ERROR at column " . (pos($s) + 1); last }   # \G: no skipping
    }
    print scalar(@tok), " tokens: @tok\n";
}
Azvya Education Pvt. Ltd.VLSI Mentor
rx/lexer.py
import re

lines = ["AND2X1 u_and (.A(n1), .B(n2), .Y(n3));",
         "AND2X1 u_and (.A(n1), #.B(n2), .Y(n3));"]
TOKEN = re.compile(r"(\s+)|(\.\w+)|(\w+)|([(),;])")


def kind(m):
    if m.group(2): return "PIN:" + m.group(2)
    if m.group(3): return "ID:" + m.group(3)
    return m.group(4)


print("-- translated with finditer (it SEARCHES, so it skips what it cannot match):")
for s in lines:
    tok = [kind(m) for m in TOKEN.finditer(s) if not m.group(1)]
    print(len(tok), "tokens:", " ".join(tok))

print("-- translated with match(s, pos), the \\G equivalent:")
for s in lines:
    tok, pos = [], 0
    while pos < len(s):
        m = TOKEN.match(s, pos)          # anchored AT pos, like \G
        if not m:
            tok.append(f"ERROR at column {pos + 1}")
            break
        if not m.group(1):
            tok.append(kind(m))
        pos = m.end()
    print(len(tok), "tokens:", " ".join(tok))
Azvya Education Pvt. Ltd.VLSI Mentor
rx_lexer.sh
#!/bin/bash
echo "--- perl rx/lexer.pl"
perl rx/lexer.pl
echo "--- python3 rx/lexer.py"
python3 rx/lexer.py
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
--- perl rx/lexer.pl
19 tokens: ID:AND2X1 ID:u_and ( PIN:.A ( ID:n1 ) , PIN:.B ( ID:n2 ) , PIN:.Y ( ID:n3 ) ) ;
9 tokens: ID:AND2X1 ID:u_and ( PIN:.A ( ID:n1 ) , ERROR at column 23
--- python3 rx/lexer.py
-- translated with finditer (it SEARCHES, so it skips what it cannot match):
19 tokens: ID:AND2X1 ID:u_and ( PIN:.A ( ID:n1 ) , PIN:.B ( ID:n2 ) , PIN:.Y ( ID:n3 ) ) ;
19 tokens: ID:AND2X1 ID:u_and ( PIN:.A ( ID:n1 ) , PIN:.B ( ID:n2 ) , PIN:.Y ( ID:n3 ) ) ;
-- translated with match(s, pos), the \G equivalent:
19 tokens: ID:AND2X1 ID:u_and ( PIN:.A ( ID:n1 ) , PIN:.B ( ID:n2 ) , PIN:.Y ( ID:n3 ) ) ;
9 tokens: ID:AND2X1 ID:u_and ( PIN:.A ( ID:n1 ) , ERROR at column 23

The Perl lexer reads the clean line as 19 tokens and stops on the corrupt line with ERROR at column 23: exactly where the # is.

The most natural Python port uses re.finditer with the same alternation. On the clean line it agrees. On the corrupt line it also reports 19 tokens, with no error, because finditer searches: when nothing matches at the #, it moves on and finds the next token. The corruption is silently skipped, and a netlist checker built on it would pass a file the original rejected.

The faithful port is pattern.match(s, pos), which anchors the attempt at pos, which is exactly \G's job. A failed match returns None, and the loop reports the column, just as the Perl else branch does. This is the construct in this chapter where a plausible port is most dangerous, because the port is more forgiving than the original, and nothing about the output looks wrong.

5. Named Captures: the Syntax, and %+

Perl writes a named group as (?<name>...) and reads it back from the %+ hash (Chapter 11.11). Python 3.13 spells the group (?P<name>...) and reads it from the match object:

Azvya Education Pvt. Ltd.VLSI Mentor
rx/named.pl
use strict;
use warnings;

my $re = qr/^\s+slack \((?<status>\w+)\)\s+(?<slack>-?[\d.]+)/;
for my $line ("  slack (VIOLATED)   -0.043", "  slack (MET)         0.120") {
    next unless $line =~ $re;
    print "$+{status} $+{slack}\n";            # %+ holds the named captures
}
Azvya Education Pvt. Ltd.VLSI Mentor
rx/named.py
import re

lines = ["  slack (VIOLATED)   -0.043", "  slack (MET)         0.120"]
try:                                         # the Perl pattern, copied as it is
    re.compile(r"^\s+slack \((?<status>\w+)\)\s+(?<slack>-?[\d.]+)")
except re.error as e:
    print(f"copied as-is: re.error: {e}")
RE = re.compile(r"^\s+slack \((?P<status>\w+)\)\s+(?P<slack>-?[\d.]+)")
for line in lines:
    m = RE.match(line)
    if m:
        print(m["status"], m["slack"])       # %+ becomes m["name"]
Azvya Education Pvt. Ltd.VLSI Mentor
rx_named.sh
#!/bin/bash
echo "--- perl rx/named.pl"
perl rx/named.pl
echo "--- python3 rx/named.py"
python3 rx/named.py
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
--- perl rx/named.pl
VIOLATED -0.043
MET 0.120
--- python3 rx/named.py
copied as-is: re.error: unknown extension ?<s at position 13
VIOLATED -0.043
MET 0.120

This one fails loudly, which makes it the easiest of the five: the copied pattern raises re.error: unknown extension ?<s at compile time. Change the spelling, and replace $+{status} with m["status"] (or m.group("status")). The one subtlety is absent groups. In Perl, $+{name} for a group that did not participate is undef, and in Python m["name"] is None. Both are falsy, but None is not the empty string, so any code that concatenates or prints it behaves differently. Chapter 28.6 measures the rest of that truth table in its section on the string "0".

6. The Implicit $_, and a Stale $1

Perl matches against $_ when no target is named (Chapter 9.7). Python has no implicit variable, so every match needs an explicit subject. That part is a mechanical change. The danger is the habit that usually comes with it: running a match without checking whether it succeeded, and reading $1 anyway. After a failed match, $1 still holds the value from the last successful match in scope (Chapter 11.10):

Azvya Education Pvt. Ltd.VLSI Mentor
rx/default_var.pl
use strict;
use warnings;

# the implicit $_ and a stale $1: two report headers, the second malformed
my @headers = ("Corner : ss_0p72v_125c", "Corner: tt_0p80v_25c");
my $corner;
for (@headers) {
    /^Corner\s+:\s+(\S+)/;                     # matches $_; result not checked
    $corner = $1;                              # after a FAILED match: the old $1
    print "$_  ->  corner=$corner\n";
}
Azvya Education Pvt. Ltd.VLSI Mentor
rx/default_var.py
import re

headers = ["Corner : ss_0p72v_125c", "Corner: tt_0p80v_25c"]
print("-- literal translation:")
for h in headers:
    try:
        corner = re.match(r"^Corner\s+:\s+(\S+)", h).group(1)
        print(f"{h}  ->  corner={corner}")
    except AttributeError as e:
        print(f"{h}  ->  AttributeError: {e}")
print("-- careful translation:")
for h in headers:
    m = re.match(r"^Corner\s+:\s+(\S+)", h)
    corner = m.group(1) if m else None
    print(f"{h}  ->  corner={corner}")
Azvya Education Pvt. Ltd.VLSI Mentor
rx_default_var.sh
#!/bin/bash
echo "--- perl rx/default_var.pl"
perl rx/default_var.pl
echo "--- python3 rx/default_var.py"
python3 rx/default_var.py
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
--- perl rx/default_var.pl
Corner : ss_0p72v_125c  ->  corner=ss_0p72v_125c
Corner: tt_0p80v_25c  ->  corner=ss_0p72v_125c
--- python3 rx/default_var.py
-- literal translation:
Corner : ss_0p72v_125c  ->  corner=ss_0p72v_125c
Corner: tt_0p80v_25c  ->  AttributeError: 'NoneType' object has no attribute 'group'
-- careful translation:
Corner : ss_0p72v_125c  ->  corner=ss_0p72v_125c
Corner: tt_0p80v_25c  ->  corner=None

The second header is malformed (Corner: without the space), so the pattern fails on it. Perl reports corner=ss_0p72v_125c: the previous header's corner, silently reused. A summary built this way attributes the second report's paths to the first corner.

The two Python ports split. The literal port calls .group(1) on None and raises AttributeError, which is loud, and arguably better than the original. The careful port checks the match and gives None, which is correct and different. Neither reproduces the Perl. That is a decision for the inventory (Chapter 28.2), not something to settle inside a translation. If the migration must be byte-identical first, the faithful port keeps the previous value on purpose (corner = m.group(1) if m else corner) with a comment that it is reproducing a defect.

7. Five Smaller Traps in One Run

Azvya Education Pvt. Ltd.VLSI Mentor
rx/traps.pl
use strict;
use warnings;

for my $lib ('C:\new', 'C:\data') {            # values that happen to hold backslashes
    (my $s = "lib=PATH") =~ s/PATH/$lib/;
    print "1 replacement value $lib : $s\n";
}
print "2 unanchored match         : ", ("  slack (MET) 0.1" =~ /slack/ ? "found" : "none"), "\n";
my @pairs = ("a=1 b=2" =~ /(\w+)=(\d+)/g);
print "3 /g with two groups       : @pairs\n";
my @whole = ("a=1 b=2" =~ /\w+=\d+/g);
print "4 /g with no groups        : @whole\n";
print "5 \\z end of string         : ", ("42\n" =~ /^\d+\z/ ? "match" : "no match"), "\n";
Azvya Education Pvt. Ltd.VLSI Mentor
rx/traps.py
import re

for lib in ["C:\\new", "C:\\data"]:            # the same characters as the Perl values
    try:
        s = re.sub("PATH", lib, "lib=PATH")
        print("1 replacement value", lib, ":", s.replace("\n", "<NEWLINE>"))
    except re.error as e:
        print("1 replacement value", lib, ": re.error:", e)
found = re.match("slack", "  slack (MET) 0.1")
print("2 unanchored match         :", "found" if found else "none")
print("3 /g with two groups       :", re.findall(r"(\w+)=(\d+)", "a=1 b=2"))
print("4 /g with no groups        :", re.findall(r"\w+=\d+", "a=1 b=2"))
try:
    hit = re.search(r"^\d+\z", "42\n")
    print("5 \\z end of string         :", "match" if hit else "no match")
except re.error as e:
    print("5 \\z end of string         : re.error:", e)
Azvya Education Pvt. Ltd.VLSI Mentor
rx_traps.sh
#!/bin/bash
echo "--- perl rx/traps.pl"
perl rx/traps.pl
echo "--- python3 rx/traps.py"
python3 rx/traps.py
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
--- perl rx/traps.pl
1 replacement value C:\new : lib=C:\new
1 replacement value C:\data : lib=C:\data
2 unanchored match         : found
3 /g with two groups       : a 1 b 2
4 /g with no groups        : a=1 b=2
5 \z end of string         : no match
--- python3 rx/traps.py
1 replacement value C:\new : lib=C:<NEWLINE>ew
1 replacement value C:\data : re.error: bad escape \d at position 2
2 unanchored match         : none
3 /g with two groups       : [('a', '1'), ('b', '2')]
4 /g with no groups        : ['a=1', 'b=2']
5 \z end of string         : re.error: bad escape \z at position 4
  1. re.sub processes backslashes in the replacement. Perl interpolates $lib and inserts its characters as they are. Python treats the replacement as a template. C:\new became C:, a newline, and ew, with no error. C:\data raised bad escape \d. Any replacement that comes from data (a path, a user string, a value read from a file) must be passed as a function (lambda m: lib), not as a string.
  2. re.match anchors at the start of the string; a Perl match does not (Chapter 11.8). /slack/ found the word in an indented line, and re.match('slack', ...) did not. Use re.search unless the Perl pattern starts with ^.
  3. and 4. re.findall changes shape with the number of groups. Perl's list-context /g returns all captures flattened, in order (Chapter 11.12). Python returns tuples when there are two or more groups, and whole matches when there are none. Code that indexes the result has to be rewritten for the shape.
  4. \z is a syntax error in Python 3.13. Use \Z, which in Python means what \z means in Perl: the absolute end of the string. (Python 3.14 adds \z as an alias.) Note that Perl's \Z and Python's \Z are not the same; Perl's allows a final newline.

8. A Parity Check for Every Pattern

Sections 2 to 7 each port one construct in isolation. A real port has many patterns, and the question that matters is whether all of them capture the same things on the inputs the script will meet. That can be checked directly. Run the original patterns and the ported ones over every line of a corpus, print what each captured, and compare the two listings. Here it is for the two patterns at the heart of the module's case study, sta_summary.pl:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if (/^Endpoint\s*:\s*(\S+)/) { ... }
if (/slack \((\w+)\)\s+(\S+)/) { ... }

The corpus is the two sample reports plus a file of line variants: the kinds of small format differences that appear between tool versions and report options. Each variant is plausible, and none is in the sample reports.

Azvya Education Pvt. Ltd.VLSI Mentor
rx/variants.rpt
Report : timing (synthetic teaching format: line variants seen across tool versions)
Endpoint: u_core/cnt_reg[5]
  Endpoint  : u_io/q_reg
  slack (MET)         .5
  slack (VIOLATED)   -0.0430
  clock skew slack (MET)   0.002
slack (MET) 0.120

The original patterns, a first port written the way many people tidy input before matching (strip the line, then match from the start), and a faithful port:

Azvya Education Pvt. Ltd.VLSI Mentor
rx/parity.pl
use strict;
use warnings;

# parity.pl FILE... - what the case study's two patterns capture, line by line
for my $file (@ARGV) {
    open my $fh, '<', $file or die "cannot read $file: $!\n";
    while (my $line = <$fh>) {
        my @hit;
        push @hit, "endpoint=$1" if $line =~ /^Endpoint\s*:\s*(\S+)/;
        push @hit, "slack=$1,$2" if $line =~ /slack \((\w+)\)\s+(\S+)/;
        print "$file:$.: @hit\n" if @hit;
    }
}
Azvya Education Pvt. Ltd.VLSI Mentor
rx/parity_first.py
import re
import sys

# a FIRST port: strip each line, then match from the start
for name in sys.argv[1:]:
    with open(name) as fh:
        for n, line in enumerate(fh, 1):
            text = line.strip()
            hit = []
            m = re.match(r"Endpoint\s*:\s*(\S+)", text)
            if m:
                hit.append(f"endpoint={m.group(1)}")
            m = re.match(r"slack \((\w+)\)\s+(\S+)", text)
            if m:
                hit.append(f"slack={m.group(1)},{m.group(2)}")
            if hit:
                print(f"{name}:{n}: {' '.join(hit)}")
Azvya Education Pvt. Ltd.VLSI Mentor
rx/parity.py
import re
import sys

# the faithful port: the raw line, re.search, and ^ only where Perl had ^
ENDPOINT = re.compile(r"^Endpoint\s*:\s*(\S+)")
SLACK = re.compile(r"slack \((\w+)\)\s+(\S+)")
for name in sys.argv[1:]:
    with open(name) as fh:
        for n, line in enumerate(fh, 1):
            hit = []
            m = ENDPOINT.search(line)
            if m:
                hit.append(f"endpoint={m.group(1)}")
            m = SLACK.search(line)
            if m:
                hit.append(f"slack={m.group(1)},{m.group(2)}")
            if hit:
                print(f"{name}:{n}: {' '.join(hit)}")

The two sample reports are needed on this page too:

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
rx_parity.sh
#!/bin/bash
# Run the original patterns and each port over every line of the corpus,
# and compare what they captured. Same lines, same captures, or a diff.
files="reports/ss_0p72v_125c.rpt reports/tt_0p80v_25c.rpt rx/variants.rpt"
perl rx/parity.pl $files > perl.txt
echo "perl: $(wc -l < perl.txt | tr -d ' ') lines captured something"
for port in parity_first parity; do
    python3 rx/$port.py $files > $port.txt
    if cmp -s perl.txt $port.txt; then
        echo "$port.py: identical captures"
    else
        echo "$port.py: differs"
        diff perl.txt $port.txt | grep '^[<>]' | sed 's/^/    /'
    fi
done
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
perl: 15 lines captured something
parity_first.py: differs
    > rx/variants.rpt:3: endpoint=u_io/q_reg
    < rx/variants.rpt:6: slack=MET,0.002
parity.py: identical captures

The first port differs on exactly two lines, both variants, in opposite directions.

It gains an endpoint the original never saw. Endpoint : u_io/q_reg is indented. The Perl pattern is anchored with ^, so it does not match, and the original never records that endpoint. Stripping the line first removes the indentation, and the port matches it. In sta_summary.pl that changes which endpoint a later violation is attributed to under -v.

It loses a slack the original counted. clock skew slack (MET) 0.002 contains slack (MET) in the middle. The unanchored Perl pattern finds it and counts it as a timing path, which is almost certainly a defect in the original: a skew value is being counted as a path slack. re.match on the stripped line requires slack at the start, so the port does not count it. It has silently fixed a defect, which changes the paths= count of any corner whose report contains such a line.

Both differences come from two habits, stripping and re.match, and both are invisible on the sample reports, which capture identically under all three implementations. That is Chapter 28.8's lesson about corpora, at the level of a single pattern. The faithful port (the raw line, re.search, and ^ only where the Perl had ^) captures identically on all fifteen lines.

Keep this check after the migration. A file of line variants with the original's captures recorded next to it is a small, permanent test for every pattern in the port. When the next tool version adds a new line format, it goes into the variants file, and the check shows at once whether the pattern still does what the original did.

9. A Porting Table

Perl constructPython rewriteWhat a literal port does instead
s/(..)/EXPR/ere.sub(pat, lambda m: ..., s), formatted like Perlinserts the expression as text, or Python's float format
$n = tr/x//s.count("x")—
tr/abc/xyz/str.translate(str.maketrans(...))a regex where . and - mean something
tr/...//cd, tr/x//sre.sub with a negated class / x+—
/\G.../gc looppattern.match(s, pos) loopfinditer silently skips what it cannot match
(?<n>...), $+{n}(?P<n>...), m["n"]re.error (loud)
/pat/ on $_, then $1explicit subject; check the matchAttributeError, or a correct value the original never produced
s/x/$value/re.sub(pat, lambda m: value, s)backslashes in value interpreted, or re.error
/pat/ (unanchored)re.searchre.match anchors at the start
list-context /gre.findall, then reshapetuples or whole matches, by group count
\z\Zre.error before Python 3.14

Use it the way Chapter 28.6's checklist is used: as a list of lines to stop at during translation, and as the review list for someone else's port.

10. Mistakes

Assuming the engine comparison settles the port. Chapter 16.8's table is about patterns. Every row above is about the code around them.

Porting /e with a replacement string. It cannot compute; it produced 0.5*1000ps without complaint.

Reading tr as a regex. tr/./_/ maps dots; re.sub('.', '_') replaced every character.

Porting a \G lexer with finditer. Measured: the corrupt netlist line produced the same 19 tokens as the clean one.

Passing data as a replacement string. A path with \n in it became a newline.

Using re.match for an unanchored Perl match. It quietly finds nothing in an indented line.

Stripping lines before matching. Measured: it made an anchored pattern match an indented line the original ignored.

11. Exercises

1. /e with a lookup. A Perl script renames clocks with s/\b(clk_\w+)/$map{$1} \/\/ $1/ge. Port it to Python, keeping unknown names unchanged, and write the three-line test that would have caught a port that replaced unknown names with None.

2. Count with tr in a condition. next if ($line =~ tr/,//) != 3; skips CSV rows that do not have exactly four fields. Port it. Then explain how Chapter 28.6's split difference (trailing empty fields) would have made a split-based port disagree with this one on a,b,,.

3. Extend the lexer. Add escaped identifiers (\bus[3] in Verilog: a backslash, any non-space characters, then a space) to both lexers. Test both on a line containing one, and on a line where the terminating space is missing. Do the two lexers still agree?

4. The stale $1 audit. Search a Perl script from your flow for matches whose result is not tested (a match statement on its own line, followed by a use of $1). For each, decide whether a failed match is possible, and what the script does when it happens.

5. Replacement safety. Find every s/// in a Perl script whose replacement interpolates a variable. Write the Python port of each with a function replacement, and a test with a value containing \1 and \n.

12. What To Remember

  • The engines mostly agree (Chapter 16.8); the operators around them are where porting takes work.
  • /e becomes a function, and the function must format numbers the way Perl did: 500ps, not 500.0ps.
  • tr/// is not a regex. Use count, str.translate, or an escaped character class. Measured: re.sub('.', '_') erased a library name.
  • A \G lexer becomes a pattern.match(s, pos) loop. Measured: finditer skipped a corrupt character and reported a clean parse.
  • Named groups become (?P<name>...) and m["name"]; the copied syntax fails loudly in Python 3.13.
  • There is no $_ in Python, and no stale $1. Measured: Perl reused the previous corner for a malformed header.
  • Never pass data as a re.sub replacement string; use a function. Measured: C:\new gained a newline.
  • re.match anchors; Perl's match does not. Use re.search.
  • findall changes shape with the group count; \z is \Z before Python 3.14.
  • Check every pattern over a corpus of line variants. Measured: a stripped re.match port gained one endpoint and lost one slack, and the sample reports showed neither.

Chapter 28.8 turns the last three chapters into a single gate: run the old and the new implementation over the same corpus and compare everything they produce, byte for byte, including the exit status and STDERR, before anything is switched over.

Where this fits

Part of the Perl curriculum.