require Class::Method::Modifiers;
my ($into, $type, $name, $code) = @_;
my $ref = ref(my $to_modify = $into->can($name));
- if ($ref && $ref =~ /Sub::Defer::Deferred/) {
- require Sub::Defer; undefer($to_modify);
+
+ # if it isn't CODE, then either we're about to die, or it's a blessed
+ # coderef - if it's a blessed coderef it might be deferred, and the
+ # user's already doing something clever so a minor speed hit is meh.
+
+ if ($ref && $ref ne 'CODE') {
+ require Sub::Defer; Sub::Defer::undefer_sub($to_modify);
}
Class::Method::Modifiers::install_modifier(@_);
}
use base qw(Exporter);
use Class::Tiny::_Utils;
-our @EXPORT = qw(defer undefer);
+our @EXPORT = qw(defer_sub undefer_sub);
our %DEFERRED;
-sub undefer {
+sub undefer_sub {
my ($deferred) = @_;
my ($target, $maker, $undeferred_ref) = @{
$DEFERRED{$deferred}||return $deferred
};
${$undeferred_ref} = my $made = $maker->();
- { no warnings 'redefine'; *{_getglob($target)} = $made }
+ if (defined($target)) {
+ no warnings 'redefine';
+ *{_getglob($target)} = $made;
+ }
return $made;
}
-sub defer {
+sub defer_sub {
my ($target, $maker) = @_;
my $undeferred;
my $deferred_string;
my $deferred = bless(sub {
- goto &{$undeferred ||= undefer($deferred_string)};
+ goto &{$undeferred ||= undefer_sub($deferred_string)};
}, 'Sub::Defer::Deferred');
$deferred_string = "$deferred";
$DEFERRED{$deferred} = [ $target, $maker, \$undeferred ];
- *{_getglob $target} = $deferred;
+ *{_getglob $target} = $deferred if defined($target);
return $deferred;
}
--- /dev/null
+package Sub::Quote;
+
+use strictures 1;
+
+sub _clean_eval { eval $_[0] }
+
+use Sub::Defer;
+use B 'perlstring';
+use base qw(Exporter);
+
+our @EXPORT = qw(quote_sub unquote_sub);
+
+our %QUOTE_OUTSTANDING;
+
+our %QUOTED;
+
+sub _unquote_all_outstanding {
+ return unless %QUOTE_OUTSTANDING;
+ my ($assembled_code, @assembled_captures, @localize_these) = '';
+ foreach my $outstanding (keys %QUOTE_OUTSTANDING) {
+ my ($name, $code, $captures) = @{$QUOTE_OUTSTANDING{$outstanding}};
+
+ push @localize_these, $name if $name;
+
+ my $make_sub = "{\n";
+
+ if (keys %$captures) {
+ my $ass_cap_count = @assembled_captures;
+ $make_sub .= join(
+ "\n",
+ map {
+ /^([\@\%\$])/
+ or die "capture key should start with \@, \% or \$: $_";
+ qq{ my ${_} = ${1}{\$_[1][${ass_cap_count}]{${\perlstring $_}}};\n};
+ } keys %$captures
+ );
+ push @assembled_captures, $captures;
+ }
+
+ my $o_quoted = perlstring $outstanding;
+ $make_sub .= (
+ $name
+ ? " no warnings 'closure';\n sub ${name} {\n"
+ : " \$Sub::Quote::QUOTED{${o_quoted}}[3] = sub {\n"
+ );
+ $make_sub .= $code;
+ $make_sub .= " }".($name ? '' : ';')."\n";
+ if ($name) {
+ $make_sub .= " \$Sub::Quote::QUOTED{${o_quoted}}[3] = \\&${name}\n";
+ }
+ $make_sub .= "}\n";
+ $assembled_code .= $make_sub;
+ }
+ if (@localize_these) {
+ $assembled_code = join("\n",
+ (map { "local *${_};" } @localize_these),
+ 'eval '.perlstring $assembled_code
+ );
+ }
+ $ENV{SUB_QUOTE_DEBUG} && warn $assembled_code;
+ _clean_eval $assembled_code, \@assembled_captures;
+ if ($@) {
+ die "Eval went very, very wrong:\n\n${assembled_code}\n\n$@";
+ }
+ %QUOTE_OUTSTANDING = ();
+}
+
+sub quote_sub {
+ # HOLY DWIMMERY, BATMAN!
+ # $name => $code => \%captures
+ # $name => $code
+ # $code => \%captures
+ # $code
+ my $captures = pop if ref($_[-1]) eq 'HASH';
+ my $code = pop;
+ my $name = $_[0];
+ my $outstanding;
+ my $deferred = defer_sub $name => sub {
+ unquote_sub($outstanding);
+ };
+ $outstanding = "$deferred";
+ $QUOTE_OUTSTANDING{$outstanding} = $QUOTED{$outstanding} = [
+ $name, $code, $captures
+ ];
+ return $deferred;
+}
+
+sub quoted_from_sub {
+ my ($sub) = @_;
+ $QUOTED{$sub};
+}
+
+sub unquote_sub {
+ my ($sub) = @_;
+ _unquote_all_outstanding;
+ $QUOTED{$sub}[3];
+}
+
+1;
my %made;
-my $one_defer = defer 'Foo::one' => sub {
+my $one_defer = defer_sub 'Foo::one' => sub {
die "remade - wtf" if $made{'Foo::one'};
$made{'Foo::one'} = sub { 'one' }
};
-my $two_defer = defer 'Foo::two' => sub {
+my $two_defer = defer_sub 'Foo::two' => sub {
die "remade - wtf" if $made{'Foo::two'};
$made{'Foo::two'} = sub { 'two' }
};
is(Foo->one, 'one', 'one (undeferred) runs');
-is(my $two_made = undefer($two_defer), $made{'Foo::two'}, 'make two');
+is(my $two_made = undefer_sub($two_defer), $made{'Foo::two'}, 'make two');
is($two_made, \&Foo::two, 'two installed');
my $three = sub { 'three' };
-is(undefer($three), $three, 'undefer non-deferred is a no-op');
+is(undefer_sub($three), $three, 'undefer non-deferred is a no-op');
done_testing;
--- /dev/null
+use strictures 1;
+use Test::More;
+use Test::Fatal;
+
+use Sub::Quote;
+
+our %EVALED;
+
+my $one = quote_sub q{
+ BEGIN { $::EVALED{'one'} = 1 }
+ 42
+};
+
+my $two = quote_sub q{
+ BEGIN { $::EVALED{'two'} = 1 }
+ 3 + $x++
+} => { '$x' => \do { my $x = 0 } };
+
+ok(!keys %EVALED, 'Nothing evaled yet');
+
+my $u_one = unquote_sub $one;
+
+is_deeply(
+ [ keys %EVALED ], [ qw(one two) ],
+ 'Both subs evaled'
+);
+
+is($one->(), 42, 'One (quoted version)');
+
+is($u_one->(), 42, 'One (unquoted version)');
+
+is($two->(), 3, 'Two (quoted version)');
+is(unquote_sub($two)->(), 4, 'Two (unquoted version)');
+is($two->(), 5, 'Two (quoted version again)');
+
+my $three = quote_sub 'Foo::three' => q{
+ $x = $_[1] if $_[1];
+ die +(caller(0))[3] if @_ > 2;
+ return $x;
+} => { '$x' => \do { my $x = 'spoon' } };
+
+is(Foo->three, 'spoon', 'get ok (named method)');
+is(Foo->three('fork'), 'fork', 'set ok (named method)');
+is(Foo->three, 'fork', 're-get ok (named method)');
+like(
+ exception { Foo->three(qw(full cutlery set)) }, qr/Foo::three/,
+ 'exception contains correct name'
+);
+
+done_testing;