Here's The Bug

Ovid on 2008-02-26T09:36:40

Yesterday's Find the Bug was pretty straightforward for folks, but I'll lay it out here. Basically, this causes the problem:

sub foo {
    while ($_ = @_) {}
}

foreach ('bar') { foo($_) }

That fails with a "Modification of a read-only value" error. This is because foreach without a declared variable implicitly creates an alias to each element in the list, in this case, a string constant. As a result, $_ has a value when foo() is called and you find yourself attempting to assign to a constant.

I've been bitten by this before and I know some of you have, so you might be asking yourself an obvious question. Why did I write "it took me a while to see [the bug]?"

Because the bug was triggered by this:

use Moose;

extends 'Some::Class';

And somehow, deep in the bowels of the Moose, it fails to load Some::Class because that class needs some other class which requires Exporter::NoWork and the string constant Some::Class gets set as the value of $_ and Exporter::NoWork has a while ($_ = shift) line in it. The first fix was strange:

use Moose;

use Some::Class;
extends 'Some::Class';

The real fix is a patch to Exporter::NoWork (though I suspect Moose is being a touch naughty, too), but it turns out that this module only has one test, and it's a load test. I hate sending patches to modules with no test suite :(

Once again, the moral of today's story: if you use anything which potentially impacts global state, please try to limit it to your local scope!


Re; Moose

Stevan on 2008-02-26T15:46:43

Well, Moose is not really doing anything very nasty here, the class loading code is quite sane (we "borrowed" it from Class::Inspector). But we are looping through @_ using $_ when we load those classes, so that is where the issue comes from.

I have adjusted all code that comes in contact with the class loader so that none of it uses $_ anymore. This should eliminate the issue you are seeing with the Exporter::NoWork and modules like it. This will be available in the next release of Class::MOP/Moose.

- Stevan

Re:Re; Moose

Ovid on 2008-02-26T16:21:33

I don't think it's terribly nasty, but the implicit aliasing can cause strange behavior if other modules don't play well with that. Imagine this:

foreach (@_) {
    some_other_function($_);
    # do something with $_
}

If some_other_function did something silly like s/(.*)/scalar reverse $1/e, then the above code would break. Guarding against side-effects is a good thing. Of course, since @_ contains aliases, I suppose that the way to be truly paranoid would be:

foreach my $var (@_ = @_) {
    some_other_function(my $temp = $var);
    # do something with $var
}

After a while, too much paranoia on my part would get ridiculous, but I'm now understanding better why Larry Wall rejected my proposal to add an 'is rw' shortcut for sub/method signatures in Perl 6 (right now you have to declare this for every argument and that gets tedious, but it's the right thing to do).

no test suite

mauzo on 2008-02-26T20:38:46

Having just got your bug reports, I'm in the middle of writing a test suite now... I wrote E::NW before I'd really cottoned on to the value of testing, and haven't touched it since. I've already found another latent bug.

Unfortunately, local $_ isn't safe either. If $_ is aliased to a member of a tied hash or array, it isn't localised properly and the value in the hash is affected as well:

tie my %h, 'Tie::StdHash';
$h{a} = 1;
for ($h{a}) {
    local $_ = 2;
    print $h{a};    # 2, but should be 1
}
print $h{a};        # back to 1 again
If you call external code in the scope of the change (as E::NW does) this could be a problem. The workarounds are either to localise all of *_ which, besides being ugly, clobbers @_ as well, or to use for instead of assigning to $_ at all. for doesn't suffer from this problem, and neither does 5.10's my $_.

Re:no test suite

Ovid on 2008-02-26T20:43:21

Oh, nice catch. We've got local workarounds in place, but I'm happy to know that you've spotted more than I did :)