The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/env perl

use strict;
use warnings;

use constant {
    EMPTY      => [],
    OPTIMIZING => [
        'accessor: inside test',
        'accessor: op_spare: 0',
        'accessor: optimizing entersub',
    ],
    OPTIMIZED => [
        'entersub: inside optimized entersub',
        'accessor: inside test',
        'accessor: op_spare: 0',
        'accessor: entersub has been optimized'
    ],
    # XXX not used: not sure we want to trigger this internal error
    DISABLING_NOT_DEFINED => [
        'entersub: inside optimized entersub',
        'entersub: disabling optimization: SV is null'
    ],
    DISABLING_NOT_CV => [
        'entersub: inside optimized entersub',
        'entersub: disabling optimization: SV is not a CV'
    ],
    DISABLING_NOT_SAME_ACCESSOR => [
        'entersub: inside optimized entersub',
        'entersub: disabling optimization: SV is not test'
    ],
    DISABLED => [
        'accessor: inside test',
        'accessor: op_spare: 1',
        'accessor: entersub optimization has been disabled'
    ],
};

use Class::XSAccessor {
    __tests__ => [ qw(foo bar) ],
    getters   => [ 'quux' ],
};

BEGIN {
    unless (Class::XSAccessor::__entersub_optimized__()) {
        print "1..0 # Skip entersub optimization not enabled", $/;
        exit;
    }
}

use Data::Dumper;
use Test::More tests => 68;

our @MESSAGES = ();

sub is_debug ($) {
    my $want = shift;

    # report errors with the caller's line number
    local $Test::Builder::Level = $Test::Builder::Level + 1;

    my $got = [ splice @MESSAGES ];

    unless (is_deeply($got, $want)) {
        local ($Data::Dumper::Terse, $Data::Dumper::Indent) = (1, 1);
        print STDERR $/, 'unmatched messages: ', Dumper($got), $/;
    }
}

local $SIG{__WARN__} = sub {
    my $warning = join '', @_;

    if ($warning =~ m{^cxah: (.+?) at \Q$0\E}) {
        push @MESSAGES, $1;
    } else {
        warn @_; # from perldoc -f warn: "__WARN__ hooks are not called from inside one"
    }
};

sub baz {
    my $self = shift;
    @_ ? $self->{baz} = shift : $self->{baz};
}

# XXX debugging note: change if/else branches to separate
# statements to debug/troubleshoot, otherwise the error will appear
# to come from the first line of the if/else statement i.e. change:
#
#     1: if ($_ == 1) {
#     2:     is_debug ...
#     3: } else {
#     4:     is_debug ... # error
#     5: }
#
#     # error at line 1
#
# to:
#
#     1: if ($_ == 1) {
#     2:     is_debug ...
#     3: }
#     4:
#     5: if ($_ == 2) {
#     6:     is_debug ... # error
#     7: }
#
#     # error at line 5

my $SELF = bless {
    foo  => 'Foo',
    bar  => 'Bar',
    baz  => 'Baz',
    quux => 'Quux'
};

# standard: verify that the accessors work as expected
for (1 .. 3) {
    is $SELF->foo, 'Foo';
    is_debug ($_ == 1 ? OPTIMIZING : OPTIMIZED);

    is $SELF->bar, 'Bar';
    is_debug ($_ == 1 ? OPTIMIZING : OPTIMIZED);

    is $SELF->baz, 'Baz';
    is_debug EMPTY;
}

# changing the CV at a call site is OK (i.e. doesn't disable
# the entersub optimization) if both CVs are the same type of
# Class::XSAccessor accessor: foo (test) -> bar (test)
for (1 .. 4) {
    my $name = [ qw(foo bar foo bar) ]->[$_ - 1];
    is $SELF->$name, ucfirst($name);

    if ($_ == 1) {
        is_debug OPTIMIZING;
    } else {
        is_debug OPTIMIZED;
    }
}

# disable the entersub optimization (method 1):
# change it to a different type of Class::XSAccessor accessor:
# foo (test) -> quux (getter)
for (1 .. 4) {
    my $name = [ qw(foo quux foo quux) ]->[$_ - 1];
    is $SELF->$name, ucfirst($name);

    if ($_ == 1) {
        is_debug OPTIMIZING;
    } elsif ($_ == 2) {
        is_debug DISABLING_NOT_SAME_ACCESSOR;
    } elsif ($_ == 3) {
        is_debug DISABLED;
    } else {
        is_debug EMPTY;
    }
}

# disable the entersub optimization (method 2):
# change it to a non-Class::XSAccessor CV: foo (test) -> baz
for (1 .. 4) {
    my $name = [ qw(foo baz foo baz) ]->[$_ - 1];
    is $SELF->$name, ucfirst($name);

    if ($_ == 1) {
        is_debug OPTIMIZING;
    } elsif ($_ == 2) {
        is_debug DISABLING_NOT_SAME_ACCESSOR;
    } elsif ($_ == 3) {
        is_debug DISABLED;
    } else {
        is_debug EMPTY;
    }
}

# if the SV passed to entersub is not a CV, disable the optimisation.
# note: the invalid type is detected in the optimised entersub,
# *not* in the accessor.
for (1 .. 4) {
    # when entersub is called in this way, the SV is a GV
    # rather than a CV
    is foo($SELF), 'Foo';

    if ($_ == 1) {
        # in the accessor (test)
        is_debug OPTIMIZING;
    } elsif ($_ == 2) {
        # the optimized entersub backs itself out
        # because the SV is a GV rather than a CV
        is_debug [ @{DISABLING_NOT_CV()}, @{DISABLED()} ];
    } else {
        # in the accessor (test)
        is_debug DISABLED;
    }
}

# confirm we haven't pessimized other call sites
for (1 .. 3) {
    is $SELF->foo, 'Foo';
    is_debug ($_ == 1 ? OPTIMIZING : OPTIMIZED);

    is $SELF->bar, 'Bar';
    is_debug ($_ == 1 ? OPTIMIZING : OPTIMIZED);

    is $SELF->baz, 'Baz';
    is_debug EMPTY;
}