#!./perl
#
# opcount.t
#
# Test whether various constructs have the right numbers of particular op
# types. This is chiefly to test that various optimisations are not
# inadvertently removed.
#
# For example the array access in sub { $a[0] } should get optimised from
# aelem into aelemfast. So we want to test that there are 1 aelemfast, 0
# aelem and 1 ex-aelem ops in the optree for that sub.

BEGIN {
    chdir 't';
    require './test.pl';
    skip_all_if_miniperl("No B under miniperl");
    @INC = '../lib';
}

use warnings;
use strict;

use B ();


{
    my %counts;

    # for a given op, increment $count{opname}. Treat null ops
    # as "ex-foo" where possible

    sub B::OP::test_opcount_callback {
        my ($op) = @_;
        my $name = $op->name;
        if ($name eq 'null') {
            my $targ = $op->targ;
            if ($targ) {
                $name = "ex-" . substr(B::ppname($targ), 3);
            }
        }
        $counts{$name}++;
    }

    # Given a code ref and a hash ref of expected op counts, check that
    # for each opname => count pair, whether that op appears that many
    # times in the op tree for that sub. If $debug is 1, display all the
    # op counts for the sub.

    sub test_opcount {
        my ($debug, $desc, $coderef, $expected_counts) = @_;

        %counts = ();
        B::walkoptree(B::svref_2object($coderef)->ROOT,
                        'test_opcount_callback');

        if ($debug) {
            note(sprintf "%3d %s", $counts{$_}, $_) for sort keys %counts;
        }

        my @exp;
        for (sort keys %$expected_counts) {
            my ($c, $e) = ($counts{$_}//0, $expected_counts->{$_});
            if ($c != $e) {
                push @exp, "expected $e, got $c: $_";
            }
        }
        ok(!@exp, $desc);
        if (@exp) {
            diag($_) for @exp;
        }
    }    
}

# aelem => aelemfast: a basic test that this test file works

test_opcount(0, "basic aelemfast",
                sub { our @a; $a[0] = 1 },
                {
                    aelem      => 0,
                    aelemfast  => 1,
                    'ex-aelem' => 1,
                }
            );

# Porting/bench.pl tries to create an empty and active loop, with the
# ops executed being exactly the same apart from the additional ops
# in the active loop. Check that this remains true.

{
    test_opcount(0, "bench.pl empty loop",
                sub { for my $x (1..$ARGV[0]) { 1; } },
                {
                     aelemfast => 1,
                     and       => 1,
                     const     => 1,
                     enteriter => 1,
                     iter      => 1,
                     leaveloop => 1,
                     leavesub  => 1,
                     lineseq   => 2,
                     nextstate => 2,
                     null      => 1,
                     pushmark  => 1,
                     unstack   => 1,
                }
            );

    no warnings 'void';
    test_opcount(0, "bench.pl active loop",
                sub { for my $x (1..$ARGV[0]) { $x; } },
                {
                     aelemfast => 1,
                     and       => 1,
                     const     => 1,
                     enteriter => 1,
                     iter      => 1,
                     leaveloop => 1,
                     leavesub  => 1,
                     lineseq   => 2,
                     nextstate => 2,
                     null      => 1,
                     padsv     => 1, # this is the additional active op
                     pushmark  => 1,
                     unstack   => 1,
                }
            );
}

#
# multideref
#
# try many permutations of aggregate lookup expressions

{
    package Foo;

    my (@agg_lex, %agg_lex, $i_lex, $r_lex);
    our (@agg_pkg, %agg_pkg, $i_pkg, $r_pkg);

    my $f;
    my @bodies = ('[0]', '[128]', '[$i_lex]', '[$i_pkg]',
                   '{foo}', '{$i_lex}', '{$i_pkg}',
                  );

    for my $prefix ('$f->()->', '$agg_lex', '$agg_pkg', '$r_lex->', '$r_pkg->')
    {
        for my $mod ('', 'local', 'exists', 'delete') {
            for my $body0 (@bodies) {
                for my $body1 ('', @bodies) {
                    for my $body2 ('', '[2*$i_lex]') {
                        my $code = "$mod $prefix$body0$body1$body2";
                        my $sub = "sub { $code }";
                        my $coderef = eval $sub
                            or die "eval '$sub': $@";

                        my %c = (aelem         => 0,
                                 aelemfast     => 0,
                                 aelemfast_lex => 0,
                                 exists        => 0,
                                 delete        => 0,
                                 helem         => 0,
                                 multideref    => 0,
                        );

                        my $top = 'aelem';
                        if ($code =~ /^\s*\$agg_...\[0\]$/) {
                            # we should expect aelemfast rather than multideref
                            $top = $code =~ /lex/ ? 'aelemfast_lex'
                                                  : 'aelemfast';
                            $c{$top} = 1;
                        }
                        else {
                            $c{multideref} = 1;
                        }

                        if ($body2 ne '') {
                            # trailing index; top aelem/exists/whatever
                            # node is kept
                            $top = $mod unless $mod eq '' or $mod eq 'local';
                            $c{$top} = 1
                        }

                        ::test_opcount(0, $sub, $coderef, \%c);
                    }
                }
            }
        }
    }
}


# multideref: ensure that the prefix expression and trailing index
# expression are optimised (include aelemfast in those expressions)


test_opcount(0, 'multideref expressions',
                sub { ($_[0] // $_)->[0]{2*$_[0]} },
                {
                    aelemfast  => 2,
                    helem      => 1,
                    multideref => 1,
                },
            );

# multideref with interesting constant indices


test_opcount(0, 'multideref const index',
                sub { $_->{1}{1.1} },
                {
                    helem      => 0,
                    multideref => 1,
                },
            );

use constant my_undef => undef;
test_opcount(0, 'multideref undef const index',
                sub { $_->{+my_undef} },
                {
                    helem      => 1,
                    multideref => 0,
                },
            );

# multideref when its the first op in a subchain

test_opcount(0, 'multideref op_other etc',
                sub { $_{foo} = $_ ? $_{bar} : $_{baz} },
                {
                    helem      => 0,
                    multideref => 3,
                },
            );

# multideref without hints

{
    no strict;
    no warnings;

    test_opcount(0, 'multideref no hints',
                sub { $_{foo}[0] },
                {
                    aelem      => 0,
                    helem      => 0,
                    multideref => 1,
                },
            );
}

# exists shouldn't clash with aelemfast

test_opcount(0, 'multideref exists',
                sub { exists $_[0] },
                {
                    aelem      => 0,
                    aelemfast  => 0,
                    multideref => 1,
                },
            );

test_opcount(0, 'barewords can be constant-folded',
             sub { no strict 'subs'; FOO . BAR },
             {
                 concat => 0,
             });

{
    use feature 'signatures';

    my @a;
    test_opcount(0, 'signature default expressions get optimised',
                 sub ($s = $a[0]) {},
                 {
                     aelem         => 0,
                     aelemfast_lex => 1,
                 });
}

# in-place sorting

{
    local our @global = (3,2,1);
    my @lex = qw(a b c);

    test_opcount(0, 'in-place sort of global',
                 sub { @global = sort @global; 1 },
                 {
                     rv2av   => 1,
                     aassign => 0,
                 });

    test_opcount(0, 'in-place sort of lexical',
                 sub { @lex = sort @lex; 1 },
                 {
                     padav   => 1,
                     aassign => 0,
                 });

    test_opcount(0, 'in-place reversed sort of global',
                 sub { @global = sort { $b <=> $a } @global; 1 },
                 {
                     rv2av   => 1,
                     aassign => 0,
                 });


    test_opcount(0, 'in-place custom sort of global',
                 sub { @global = sort {  $a<$b?1:$a>$b?-1:0 } @global; 1 },
                 {
                     rv2av   => 1,
                     aassign => 0,
                 });

    sub mysort { $b cmp $a };
    test_opcount(0, 'in-place sort with function of lexical',
                 sub { @lex = sort mysort @lex; 1 },
                 {
                     padav   => 1,
                     aassign => 0,
                 });


}

# in-place assign optimisation for @a = split

{
    local our @pkg;
    my @lex;

    for (['@pkg',       0, ],
         ['local @pkg', 0, ],
         ['@lex',       0, ],
         ['my @a',      0, ],
         ['@{[]}',      1, ],
    ){
        # partial implies that the aassign has been optimised away, but
        # not the rv2av
        my ($code, $partial) = @$_;
        test_opcount(0, "in-place assignment for split: $code",
                eval qq{sub { $code = split }},
                {
                    padav   => 0,
                    rv2av   => $partial,
                    aassign => 0,
                });
    }
}

# index(...) == -1 and variants optimise away the EQ/NE/etc and CONST
# and with $lex = (index(...) == -1), the assignment is optimised away
# too

{
    local our @pkg;
    my @lex;

    my ($x, $y, $z);
    for my $assign (0, 1) {
        for my $index ('index($x,$y)', 'rindex($x,$y)') {
            for my $fmt (
                    "%s <= -1",
                    "%s == -1",
                    "%s != -1",
                    "%s >  -1",

                    "%s <  0",
                    "%s >= 0",

                    "-1 <  %s",
                    "-1 == %s",
                    "-1 != %s",
                    "-1 >= %s",

                    " 0 <= %s",
                    " 0 >  %s",

            ) {
                my $expr = sprintf $fmt, $index;
                $expr = "\$z = ($expr)" if $assign;

                test_opcount(0, "optimise away compare,const in $expr",
                        eval qq{sub { $expr }},
                        {
                            lt      => 0,
                            le      => 0,
                            eq      => 0,
                            ne      => 0,
                            ge      => 0,
                            gt      => 0,
                            const   => 0,
                            sassign => 0,
                            padsv   => 2.
                        });
            }
        }
    }
}


# a sprintf that can't be optimised shouldn't stop the .= concat being
# optimised

{
    my ($i,$j,$s);
    test_opcount(0, "sprintf pessimised",
        sub { $s .= sprintf "%d%d",$i, $j },
        {
            const       => 1,
            sprintf     => 1,
            concat      => 0,
            multiconcat => 1,
            padsv       => 2,
        });
}


# sprintf with constant args should be constant folded

test_opcount(0, "sprintf constant args",
        sub { sprintf "%s%s", "abc", "def" },
        {
            const       => 1,
            sprintf     => 0,
            multiconcat => 0.
        });

#
# concats and assigns that should be optimised into a single multiconcat
# op

{

    my %seen; # weed out duplicate combinations

    # these are the ones where using multiconcat isn't a gain, so should
    # be pessimised
    my %pessimise = map { $_ => 1 }
                        '$a1.$a2',
                        '"$a1$a2"',
                        '$pkg .= $a1',
                        '$pkg .= "$a1"',
                        '$lex  = $a1.$a2',
                        '$lex  = "$a1$a2"',
                        # these already constant folded
                        'sprintf("-")',
                        '$pkg  = sprintf("-")',
                        '$lex  = sprintf("-")',
                        'my $l = sprintf("-")',
                    ;

    for my $lhs (
        '',
        '$pkg  = ',
        '$pkg .= ',
        '$lex  = ',
        '$lex .= ',
        'my $l = ',
    ) {
        for my $nargs (0..3) {
            for my $type (0..2) {
                # 0: $a . $b
                # 1: "$a$b"
                # 2: sprintf("%s%s", $a, $b)

                for my $const (0..4) {
                    # 0: no consts:       "$a1$a2"
                    # 1: interior consts: "$a1-$a2"
                    # 2: + LH   edge:    "-$a1-$a2"
                    # 3: + RH   edge:     "$a1-$a2-"
                    # 4: + both edge:    "-$a1-$a2-"

                    my @args;
                    my @sprintf_args;
                    my $c = $type == 0 ? '"-"' : '-';
                    push @args, $c if $const == 2 || $const == 4;
                    for my $n (1..$nargs) {
                        if ($type == 2) {
                            # sprintf
                            push @sprintf_args, "\$a$n";
                            push @args, '%s';
                        }
                        else {
                            push @args, "\$a$n";
                        }
                        push @args, $c if $const;
                    }
                    pop @args if  $const == 1 || $const == 2;

                    push @args, $c if $nargs == 0 && $const == 1;


                    if ($type == 2) {
                        # sprintf
                        next unless @args;
                    }
                    else {
                        # To ensure that there's at least once concat
                        # action, if appending, need at least one RHS arg;
                        # else least 2 args:
                        #    $x = $a . $b
                        #    $x .= $a
                        next unless @args >= ($lhs =~ /\./ ? 1 : 2);
                    }

                    my $rhs;
                    if ($type == 0) {
                        $rhs = join('.', @args);
                    }
                    elsif ($type == 1) {
                        $rhs = '"' . join('',  @args) . '"'
                    }
                    else {
                        $rhs = 'sprintf("'
                               . join('',  @args)
                               . '"'
                               . join('', map ",$_",  @sprintf_args)
                               . ')';
                    }

                    my $expr = $lhs . $rhs;

                    next if exists $seen{$expr};
                    $seen{$expr} = 1;

                    my ($a1, $a2, $a3);
                    my $lex;
                    our $pkg;
                    my $sub = eval qq{sub { $expr }};
                    die "eval(sub { $expr }: $@" if $@;

                    my $pm = $pessimise{$expr};
                    test_opcount(0, ($pm ? "concat     " : "multiconcat")
                                            . ": $expr",
                            $sub,
                            $pm
                            ?   {   multiconcat => 0 }
                            :   {
                                    multiconcat => 1,
                                    padsv       => $nargs,
                                    concat      => 0,
                                    sprintf     => 0,
                                    const       => 0,
                                    sassign     => 0,
                                    stringify   => 0,
                                    gv          => 0, # optimised to gvsv
                                });
                }
            }
        }
    }
}

# $lex = "foo" should *not* get converted into a multiconcat - there's
# no actual concatenation involved, and treating it as a degnerate concat
# would forego any COW copy efficiency

test_opcount(0, '$lex = "foo"', sub { my $x; $x = "foo"; },
        {
            multiconcat => 0,
        });

# for '$lex1 = $lex2 . $lex3', multiconcat is normally slower than
# concat, except in the specific case of '$lex1 = $lex2 . $lex1'

test_opcount(0, '$lex1 = $lex2 . $lex1', sub { my ($x,$y); $x = $y . $x },
            {
                multiconcat => 1,
                padsv       => 4, # 2 are from the my()
                concat      => 0,
                sassign     => 0,
                stringify   => 0,
            });
test_opcount(0, '$lex1 = "$lex2$lex1"', sub { my ($x,$y); $x = "$y$x" },
            {
                multiconcat => 1,
                padsv       => 4, # 2 are from the my()
                concat      => 0,
                sassign     => 0,
                stringify   => 0,
            });
test_opcount(0, '$lex1 = $lex1 . $lex1', sub { my $x; $x = $x . $x },
            {
                multiconcat => 0,
            });

# 'my $x .= ...' doesn't make a lot of sense and so isn't optimised
test_opcount(0, 'my $a .= $b.$c.$d', sub { our ($b,$c,$d); my $a .= $b.$c.$d },
            {
                padsv => 1,
            });

# prefer rcatline optimisation over multiconcat

test_opcount(0, "rcatline", sub { my ($x,$y); open FOO, "xxx"; $x .= <FOO> },
        {
            rcatline    => 1,
            readline    => 0,
            multiconcat => 0,
            concat      => 0,
        });

# long chains of concats should be converted into chained multiconcats

{
    my @a;
    for my $i (60..68) { # check each side of 64 threshold
        my $c = join '.', map "\$a[$_]", 1..$i;
        my $sub = eval qq{sub { $c }} or die $@;
        test_opcount(0, "long chain $i", $sub,
            {
                multiconcat => $i > 65 ? 2 : 1,
                concat      => $i == 65 ? 1 : 0,
                aelem       => 0,
                aelemfast   => 0,
            });
    }
}

# with C<$state $s = $a . $b . ....>, the assign is optimised away,
# but the padsv isn't (it's treated like a general LHS expression rather
# than using OPpTARGET_MY).

test_opcount(0, "state works with multiconcat",
                sub { use feature 'state'; our ($a, $b, $c); state $s = $a . $b . $c },
                {
                    multiconcat => 1,
                    concat      => 0,
                    sassign     => 0,
                    once        => 1,
                    padsv       => 2, # one each for the next/once branches
                });

# multiple concats of constants preceded by at least one non-constant
# shouldn't get constant-folded so that a concat overload method is called
# for each arg. So every second constant string is left as an OP_CONST

test_opcount(0, "multiconcat: 2 adjacent consts",
                sub { my ($a, $b); $a = $b . "c" . "d" },
                {
                    const       => 1,
                    multiconcat => 1,
                    concat      => 0,
                    sassign     => 0,
                });
test_opcount(0, "multiconcat: 3 adjacent consts",
                sub { my ($a, $b); $a = $b . "c" . "d" . "e" },
                {
                    const       => 1,
                    multiconcat => 1,
                    concat      => 0,
                    sassign     => 0,
                });
test_opcount(0, "multiconcat: 4 adjacent consts",
                sub { my ($a, $b); $a = $b . "c" . "d" . "e" ."f" },
                {
                    const       => 2,
                    multiconcat => 1,
                    concat      => 0,
                    sassign     => 0,
                });

# multiconcat shouldn't include the assign if the LHS has 'local'

test_opcount(0, "multiconcat: local assign",
                sub { our $global; local $global = "$global-X" },
                {
                    const       => 0,
                    gvsv        => 2,
                    multiconcat => 1,
                    concat      => 0,
                    sassign     => 1,
                });

{
    use feature 'try';
    no warnings 'experimental::try';

    test_opcount(0, "try/catch: catch block is optimized",
                    sub { my @a; try {} catch($e) { $a[0] } },
                    {
                        aelemfast_lex => 1,
                        aelem         => 0,
                    });
}

{
    use feature 'defer';
    no warnings 'experimental::defer';

    test_opcount(0, "pushdefer: block is optimized",
                    sub { my @a; defer { $a[0] } },
                    {
                        aelemfast_lex => 1,
                        aelem         => 0,
                    });
}

# builtin:: function calls should be replaced with efficient op implementations
no warnings 'experimental::builtin';

test_opcount(0, "builtin::true/false are replaced with constants",
                sub { my $x = builtin::true(); my $y = builtin::false() },
                {
                    entersub => 0,
                    const    => 2,
                });

test_opcount(0, "builtin::is_bool is replaced with direct opcode",
                sub { my $x; my $y; $y = builtin::is_bool($x); },
                {
                    entersub => 0,
                    is_bool  => 1,
                    padsv    => 3, # OA_TARGLEX applies so only 3, not 4
                    sassign  => 0,
                });

test_opcount(0, "builtin::is_bool gets constant-folded",
                sub { builtin::is_bool(123); },
                {
                    entersub => 0,
                    is_bool  => 0,
                    const    => 1,
                });

test_opcount(0, "builtin::weaken is replaced with direct opcode",
                sub { my $x = []; builtin::weaken($x); },
                {
                    entersub => 0,
                    weaken   => 1,
                });

test_opcount(0, "builtin::unweaken is replaced with direct opcode",
                sub { my $x = []; builtin::unweaken($x); },
                {
                    entersub => 0,
                    unweaken => 1,
                });

test_opcount(0, "builtin::is_weak is replaced with direct opcode",
                sub { builtin::is_weak([]); },
                {
                    entersub => 0,
                    is_weak  => 1,
                });

test_opcount(0, "builtin::blessed is replaced with direct opcode",
                sub { builtin::blessed([]); },
                {
                    entersub => 0,
                    blessed  => 1,
                });

test_opcount(0, "builtin::refaddr is replaced with direct opcode",
                sub { builtin::refaddr([]); },
                {
                    entersub => 0,
                    refaddr  => 1,
                });

test_opcount(0, "builtin::reftype is replaced with direct opcode",
                sub { builtin::reftype([]); },
                {
                    entersub => 0,
                    reftype  => 1,
                });

my $one_point_five = 1.5;   # Prevent const-folding.
test_opcount(0, "builtin::ceil is replaced with direct opcode",
                sub { builtin::ceil($one_point_five); },
                {
                    entersub => 0,
                    ceil     => 1,
                });

test_opcount(0, "builtin::floor is replaced with direct opcode",
                sub { builtin::floor($one_point_five); },
                {
                    entersub => 0,
                    floor    => 1,
                });

done_testing();
