Search code examples
perlswitch-statementmatchgiven

Hidden bugs with given-when and for-match. Is Perl truly cross-platform?


I have been trying to make a relatively large Perl program that has been working perfectly fine on CentOS for many years to work on Ubuntu and this has become a huge nightmare. CentOS uses Perl built for x86_64-linux-thread-multi and Ubuntu uses the x86_64-linux-gnu-thread-multi build. AFAIK, the interpreter behavior should be the same in both environments when the program invokes the same previous version v5.10.1. However I have been getting very different behavior, including warnings about given/when and smartmatch being experimental and, most importantly, a set of nasty bugs that are hard to trace and resolve. A particular problem occurs when a given statement shown below (form 1) matches and calls a function. Then suddenly the value of the switch variable (called $ailtype) that is never otherwise touched gets erased from memory! If I simply call that function nothing obnoxious happens. So, I replaced the given/when usage with a for statement (form 2) and my question is why does the the problem still occur?! The only form that truly avoids the problem is a simple chain of if/elsifs (form 3) and this clearly shows that the problem is with forms 1 and 2 and the perl interpreter being inconsistent and bug-ridden: it does not even produce an "experimental" warning for form 2.

Here is form 1 (original):

print "ailtype is $ailtype \n"; # prints "ailtype is 8"

given ($ailtype) {
    when (4) { &parse_mascot}
    when (5) { &parse_sequest}
    when (8) { &parse_spectrast($ms2_results, $rttemp)}
    when (9) { &parse_cnstab($ms2_results, $rttemp)}
    default { STDOUT->autoflush(1) and die "ailtype=|$ailtype| unknown.\n"; }
}

print "ailtype is $ailtype \n"; # prints "ailtype is ". $ailtype got destroyed!

The prints are for debug. I can put them inside the when block and confirm that $ailtype gets destroyed after the &parse_spectrast function call. However, the function does not read or touch $ailtype at all! (Interestingly, if I go inside the function and print the value of $ailtype to find exactly where it gets messed up, I see that it occurs within a while loop parsing the lines of an input file. Printing $ailtype somehow returns the lines of that file!)

The program consists of several large perl files with many given/when statements and re-writing all of them by hand would be tedious. I have to make sure that the alternative form works. So I tried form 2 (suggested here):

print "ailtype is $ailtype \n"; # prints "ailtype is 8"

for ($ailtype) {
    /4/ and do { &parse_mascot; last};
    /5/ and do { &parse_sequest; last};
    /8/ and do { &parse_spectrast($ms2_results, $rttemp); last};
    /9/ and do { &parse_cnstab($ms2_results, $rttemp); last};
    do { STDOUT->autoflush(1) and die "ailtype=|$ailtype| unknown.\n"; }
}

print "ailtype is $ailtype \n"; # prints "ailtype is ". $ailtype still gets destroyed!

The problem still occurs in this form, and I really don't understand why?! The interpreter no longer warns about experimental given/when here (they're still used elsewhere in the code though. I ensured they didn't occur before the problematic block and that didn't help).

Surprising or not, a chain of if/elsifs (form 3) works fine:

print "ailtype is $ailtype \n"; # prints "ailtype is 8"

if    (4 == $ailtype) { 
    &parse_mascot;
}
elsif (5 == $ailtype) { 
    &parse_sequest;
}
elsif (8 == $ailtype) { 
    &parse_spectrast($ms2_results, $rttemp); 
}
elsif (9 == $ailtype) { 
    &parse_cnstab($ms2_results, $rttemp);
}
else { 
    STDOUT->autoflush(1) and die "ailtype=|$ailtype| unknown.\n";
}

print "ailtype is $ailtype \n"; # prints "ailtype is 8". It was never changed.

But I thought the powers of Perl were there to save us from having to code all of this. I would be willing to do so if I was sure I had an otherwise reliable interpreter. Changing the version to v5.16.3 (the latest installed on both platforms) only produces new errors regarding declarations, "bareword STDOUT not allowed", etc. Having fought with this bug alone for 10 hours, I am seriously in doubt.


Solution

  • for aliases $_ for each value in the loop. If the called function changes the value of $_, the original variable will be changed, as well. The diamond operator used in a while loop changes the global $_ and it becomes undef on the eof.

    #!/usr/bin/perl
    use warnings;
    use strict;
    
    sub f {
        while (<DATA>) {
            warn "read:\t$_";
        }
    }
    
    my $x = 8;
    
    print "Before:<<$x>>\n";
    
    for ($x) {
        &f();
    }
    
    print "After:<<$x>>\n";
    
    __DATA__
    A
    B
    

    Solution: Insert the following line before the while (<>) {:

        local $_;
    

    This will restore the original value of $_ when leaving the scope of the local statement.