Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I inject multiple lines with Devel::Declare?

Tags:

perl

I want to use Devel::Declare to inject multiple lines of Perl code. However, Devel::Declare::set_linestr() cannot deal with multiple lines.

Normally I would join multiple statements together as a single line. These statements must be on separate lines to preserve their line numbers for error reporting purposes. This is to solve this bug in Method::Signatures and this related bug. I'm open to alternative solutions.

For example, Method::Signatures currently turns this code...

use Method::Signatures;

func hello(
    $who = "World",
    $greeting = get_greeting($who)
) {
    die "$greeting, $who";
}

...into this...

func  \&hello; sub hello  { BEGIN { Method::Signatures->inject_scope('') }; my $who = (@_ > 0) ? ($_[0]) : ( get_greeting($who)); my $greeting = (@_ > 1) ? ($_[1]) : ( "Hello"); Method::Signatures->too_many_args_error(2) if @_ > 2;
    die "$greeting, $who";
}

die $who then reports line 4 instead of line 7.

I would like it to instead be this (or perhaps something involving #line).

func  \&hello; sub hello  { BEGIN { Method::Signatures->inject_scope('') };
    my $who = (@_ > 0) ? ($_[0]) : ( "World");
    my $greeting = (@_ > 1) ? ($_[1]) : ( get_greeting($who));
    Method::Signatures->too_many_args_error(2) if @_ > 2;
    die "$greeting, $who";
}

Not only does this faithfully reproduce the line numbers, should get_greeting croak it will report having been called from the correct line.

like image 839
Schwern Avatar asked Aug 06 '14 22:08

Schwern


2 Answers

As per your own answer, the following works:

sub __empty() { '' }

sub parse_proto {
    my $self = shift;
    return q[print __LINE__."\n"; Foo::__empty(
);print __LINE__."\n"; Foo::__empty(
);print __LINE__."\n";];
}

But introduces unacceptable overhead because the __empty() function must be called for every parameter. The overhead can be eliminated by calling __empty() conditionally using a condition which will never evaluate to true.

sub __empty() { '' }

sub parse_proto {
    my $self = shift;
    return q[print __LINE__."\n"; 0 and Foo::__empty(
);print __LINE__."\n"; 0 and Foo::__empty(
);print __LINE__."\n";];
}
like image 63
tobyink Avatar answered Nov 20 '22 16:11

tobyink


I figured out a way to do it, but it's hacky and slow.

I noticed you could inject strings with literal newlines in them and they'd work. I reckon the parser knows enough to keep going past a newline in a string. I figured one can exploit that to trick the parser to keep going.

package Foo;

use strict;
use warnings;
use v5.12;

use parent "Devel::Declare::MethodInstaller::Simple";

sub import {
    my $class = shift;
    my $caller = caller;

    $class->install_methodhandler(
        into            => $caller,
        name            => 'method'
    );
}

sub parse_proto {
    my $self = shift;
    return q[print __LINE__."\n"; my $__empty = q{
};print __LINE__."\n"; $__empty = q{
};print __LINE__."\n";];
}

1;

And it works... except __LINE__ doesn't get incremented. Glitch in the Perl parser? I tried a regex with a newline in it, that didn't increment the line either.

But a subroutine does work!

sub __empty() { '' }

sub parse_proto {
    my $self = shift;
    return q[print __LINE__."\n"; Foo::__empty(
);print __LINE__."\n"; Foo::__empty(
);print __LINE__."\n";];
}

And it's a constant subroutine call, Perl should optimize it out, right? Alas, no. It seems the newline in the call fools the optimizer. On the upside, this avoids a "Useless use of a constant in void context" warning. On the down side, it introduces a subroutine call for each parameter and that's an unacceptable amount of overhead to add to every subroutine call.

Maybe someone else can come up with a clever way to squeak a newline into Perl syntax?

like image 43
Schwern Avatar answered Nov 20 '22 15:11

Schwern