Search code examples
listperldebuggingevalstack-trace

How to get the current line number in a multi-line list initializer of testcases?


Is there a way to reliably get the current line number during a Perl multiline list assignment without explicitly using __LINE__? I am storing testcases in a list and would like to tag each with its line number.* That way I can do (roughly) ok($_->[1], 'line ' . $_->[0]) for @tests. And, of course, I would like to save typing compared to putting __LINE__ at the beginning of each test case :) . I have not been able to find a way to do so, and I have encountered some confusing behaviour in the lines reported by caller.

* Possible XY, but I can't find a module to do it.

Update I found a hack and posted it as an answer. Thanks to @zdim for helping me think about the problem a different way!

MCVE

A long one, because I've tried several different options. my_eval, L(), and L2{} are some I've tried so far — L() was the one I initially hoped would work. Jump down to my @testcases to see how I'm using these. When testing, do copy the shebang line.

Here's my non-MCVE use case, if you are interested.

#!perl
use strict; use warnings; use 5.010;

# Modified from https://www.effectiveperlprogramming.com/2011/06/set-the-line-number-and-filename-of-string-evals/#comment-155 by http://sites.google.com/site/shawnhcorey/
sub my_eval {
    my ( $expr ) = @_;
    my ( undef, $file, $line ) = caller;
    my $code = "# line $line \"$file\"\n" . $expr;

    unless(defined wantarray) {
        eval $code; die $@ if $@;
    } elsif(wantarray) {
        my @retval = eval $code; die $@ if $@; return @retval;
    } else {
        my $retval = eval $code; die $@ if $@; return $retval;
    }
}

sub L {     # Prepend caller's line number
    my (undef, undef, $line) = caller;
    return ["$line", @_];
} #L

sub L2(&) {     # Prepend caller's line number
    my $fn = shift;
    my (undef, undef, $line) = caller;
    return ["$line", &$fn];
} #L2

# List of [line number, item index, expected line number, type]
my @testcases = (
    ([__LINE__,0,32,'LINE']),
    ([__LINE__,1,33,'LINE']),
    (L(2,34,'L()')),
    (L(3,35,'L()')),
    (do { L(4,36,'do {L}') }),
    (do { L(5,37,'do {L}') }),
    (eval { L(6,38,'eval {L}') }),
    (eval { L(7,39,'eval {L}') }),
    (eval "L(8,40,'eval L')"),
    (eval "L(9,41,'eval L')"),
    (my_eval("L(10,42,'my_eval L')")),
    (my_eval("L(11,43,'my_eval L')")),
    (L2{12,44,'L2{}'}),
    (L2{13,45,'L2{}'}),
);

foreach my $idx (0..$#testcases) {
    printf "%2d %-10s line %2d expected %2d %s\n",
            $idx, $testcases[$idx]->[3], $testcases[$idx]->[0],
            $testcases[$idx]->[2],
            ($testcases[$idx]->[0] != $testcases[$idx]->[2]) && '*';
}

Output

With my comments added.

 0 LINE       line 32 expected 32
 1 LINE       line 33 expected 33

Using __LINE__ expressly works fine, but I'm looking for an abbreviation.

 2 L()        line 45 expected 34 *
 3 L()        line 45 expected 35 *

L() uses caller to get the line number, and reports a line later in the file (!).

 4 do {L}     line 36 expected 36
 5 do {L}     line 45 expected 37 *

When I wrap the L() call in a do{}, caller returns the correct line number — but only once (!).

 6 eval {L}   line 38 expected 38
 7 eval {L}   line 39 expected 39

Block eval, interestingly, works fine. However, it's no shorter than __LINE__.

 8 eval L     line  1 expected 40 *
 9 eval L     line  1 expected 41 *

String eval gives the line number inside the eval (no surprise)

10 my_eval L  line 45 expected 42 *
11 my_eval L  line 45 expected 43 *

my_eval() is a string eval plus a #line directive based on caller. It also gives a line number later in the file (!).

12 L2{}       line 45 expected 44 *
13 L2{}       line 45 expected 45

L2 is the same as L, but it takes a block that returns a list, rather than the list itself. It also uses caller for the line number. And it is correct once, but not twice (!). (Possibly just because it's the last item — my_eval reported line 45 also.)

So, what is going on here? I have heard of Deparse and wonder if this is optimization-related, but I don't know enough about the engine to know where to start investigating. I also imagine this could be done with source filters or Devel::Declare, but that is well beyond my level of experience.

Take 2

@zdim's answer got me started thinking about fluent interfaces, e.g., as in my answer:

$testcases2     # line 26
    ->add(__LINE__,0,27,'LINE')
    ->add(__LINE__,1,28,'LINE')
    ->L(2,29,'L()')
    ->L(3,30,'L()')
    ->L(3,31,'L()')
;

However, even those don't work here — I get line 26 for each of the ->L() calls. So it appears that caller sees all of the chained calls as coming from the $testcases2->... line. Oh well. I'm still interested in knowing why, if anyone can enlighten me!


Solution

  • Edit This answer is now wrapped in a CPAN module (GitHub)!


    @zdim's answer got me thinking about fluent interfaces. Below are two hacks that work for my particular use case, but that don't help me understand the behaviour reported in the question. If you can help, please post another answer!

    Hack 2 (newer) (the one now on CPAN)

    I think this one is very close to minimal. In perl, you can call a subroutine through a reference with $ref->(), and you can leave out the second and subsequent -> in a chain of arrows. That means, for example, that you can do:

    my $foo; $foo=sub { say shift; return $foo; };
    $foo->(1)
          (2)
          (3);
    

    Looks good, right? So here's the MCVE:

    #!perl
    use strict; use warnings; use 5.010;
    
    package FluentAutoIncList2 {
        sub new {   # call as $class->new(__LINE__); each element is one line
            my $class = shift;
            my $self = bless {lnum => shift // 0, arr => []}, $class;
    
            # Make a loader that adds an item and returns itself --- not $self
            $self->{loader} = sub { $self->L(@_); return $self->{loader} };
    
            return $self;
        }
        sub size { return scalar @{ shift->{arr} }; }
        sub last { return shift->size-1; }      # $#
    
        sub load { goto &{ shift->{loader} } }  # kick off loading
    
        sub L {     # Push a new record with the next line number on the front
            my $self = shift;
            push @{ $self->{arr} }, [++$self->{lnum}, @_];
            return $self;
        } #L
    
        sub add {   # just add it
            my $self = shift;
            ++$self->{lnum};    # keep it consistent
            push @{ $self->{arr} }, [@_];
            return $self;
        } #add
    
    } #FluentAutoIncList2
    
    # List of [line number, item index, expected line number, type]
    my $testcases = FluentAutoIncList2->new(__LINE__)    # line 28
        ->add(__LINE__,0,36,'LINE')
        ->add(__LINE__,1,37,'LINE');
        $testcases->load(2,38,'load')->     # <== Only need two arrows.
        (3,39,'chain load')                 # <== After that, () are enough.
        (4,40,'chain load')
        (5,41,'chain load')
        (6,42,'chain load')
        (7,43,'chain load')
    ;
    
    foreach my $idx (0..$testcases->last) {
        printf "%2d %-10s line %2d expected %2d %s\n",
                $idx, $testcases->{arr}->[$idx]->[3],
                $testcases->{arr}->[$idx]->[0],
                $testcases->{arr}->[$idx]->[2],
                ($testcases->{arr}->[$idx]->[0] !=
                    $testcases->{arr}->[$idx]->[2]) && '*';
    }
    

    Output:

     0 LINE       line 36 expected 36
     1 LINE       line 37 expected 37
     2 load       line 38 expected 38
     3 chain load line 39 expected 39
     4 chain load line 40 expected 40
     5 chain load line 41 expected 41
     6 chain load line 42 expected 42
     7 chain load line 43 expected 43
    

    All the chain load lines were loaded with zero extra characters compared to the original [x, y] approach. Some overhead, but not much!

    Hack 1

    Code:

    By starting with __LINE__ and assuming a fixed number of lines per call, a counter will do the trick. This could probably be done more cleanly with a tie.

    #!perl
    use strict; use warnings; use 5.010;
    
    package FluentAutoIncList {
        sub new {   # call as $class->new(__LINE__); each element is one line
            my $class = shift;
            return bless {lnum => shift // 0, arr => []}, $class;
        }
        sub size { return scalar @{ shift->{arr} }; }
        sub last { return shift->size-1; }      # $#
    
        sub L {     # Push a new record with the next line number on the front
            my $self = shift;
            push @{ $self->{arr} }, [++$self->{lnum}, @_];
            return $self;
        } #L
    
        sub add {   # just add it
            my $self = shift;
            ++$self->{lnum};    # keep it consistent
            push @{ $self->{arr} }, [@_];
            return $self;
        } #add
    
    } #FluentAutoIncList
    
    # List of [line number, item index, expected line number, type]
    my $testcases = FluentAutoIncList->new(__LINE__)    # line 28
        ->add(__LINE__,0,29,'LINE')
        ->add(__LINE__,1,30,'LINE')
        ->L(2,31,'L()')
        ->L(3,32,'L()')
        ->L(4,33,'L()')
    ;
    
    foreach my $idx (0..$testcases->last) {
        printf "%2d %-10s line %2d expected %2d %s\n",
                $idx, $testcases->{arr}->[$idx]->[3],
                $testcases->{arr}->[$idx]->[0],
                $testcases->{arr}->[$idx]->[2],
                ($testcases->{arr}->[$idx]->[0] !=
                    $testcases->{arr}->[$idx]->[2]) && '*';
    }
    

    Output:

     0 LINE       line 29 expected 29
     1 LINE       line 30 expected 30
     2 L()        line 31 expected 31
     3 L()        line 32 expected 32
     4 L()        line 33 expected 33