From: David Leadbeater Date: Tue, 11 Jan 2011 21:19:38 +0000 (+0000) Subject: Add persistent hints X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?p=p5sagit%2FEval-WithLexicals.git;a=commitdiff_plain;h=148445b9606e76f0bc86d2f5bfd5d2f8588ceb0f Add persistent hints (docs still to write) --- diff --git a/lib/Eval/WithLexicals.pm b/lib/Eval/WithLexicals.pm index fab265c..7028702 100644 --- a/lib/Eval/WithLexicals.pm +++ b/lib/Eval/WithLexicals.pm @@ -1,110 +1,12 @@ package Eval::WithLexicals; use Moo; -use Sub::Quote; our $VERSION = '1.001000'; # 1.1.0 $VERSION = eval $VERSION; -has lexicals => (is => 'rw', default => quote_sub q{ {} }); - -{ - my %valid_contexts = map +($_ => 1), qw(list scalar void); - - has context => ( - is => 'rw', default => quote_sub(q{ 'list' }), - isa => sub { - my ($val) = @_; - die "Invalid context type $val - should be list, scalar or void" - unless $valid_contexts{$val}; - }, - ); -} - -has in_package => ( - is => 'rw', default => quote_sub q{ 'Eval::WithLexicals::Scratchpad' } -); - -has prelude => ( - is => 'rw', default => quote_sub q{ 'use strictures 1;' } -); - -sub eval { - my ($self, $to_eval) = @_; - local *Eval::WithLexicals::Cage::current_line; - local *Eval::WithLexicals::Cage::pad_capture; - local *Eval::WithLexicals::Cage::grab_captures; - my $setup = Sub::Quote::capture_unroll('$_[2]', $self->lexicals, 2); - my $package = $self->in_package; - my $prelude = $self->prelude; - local our $current_code = qq!${prelude} -${setup} -sub Eval::WithLexicals::Cage::current_line { -package ${package}; -#line 1 "(eval)" -${to_eval} -;sub Eval::WithLexicals::Cage::pad_capture { } -BEGIN { Eval::WithLexicals::Util::capture_list() } -sub Eval::WithLexicals::Cage::grab_captures { - no warnings 'closure'; no strict 'vars'; - package Eval::WithLexicals::VarScope;!; - $self->_eval_do(\$current_code, $self->lexicals, $to_eval); - my @ret; - my $ctx = $self->context; - if ($ctx eq 'list') { - @ret = Eval::WithLexicals::Cage::current_line(); - } elsif ($ctx eq 'scalar') { - $ret[0] = Eval::WithLexicals::Cage::current_line(); - } else { - Eval::WithLexicals::Cage::current_line(); - } - $self->lexicals({ - %{$self->lexicals}, - %{$self->_grab_captures}, - }); - @ret; -} - -sub _grab_captures { - my ($self) = @_; - my $cap = Eval::WithLexicals::Cage::grab_captures(); - foreach my $key (keys %$cap) { - my ($sigil, $name) = $key =~ /^(.)(.+)$/; - my $var_scope_name = $sigil.'Eval::WithLexicals::VarScope::'.$name; - if ($cap->{$key} eq eval "\\${var_scope_name}") { - delete $cap->{$key}; - } - } - $cap; -} - -sub _eval_do { - my ($self, $text_ref, $lexicals, $original) = @_; - local @INC = (sub { - if ($_[1] eq '/eval_do') { - open my $fh, '<', $text_ref; - $fh; - } else { - (); - } - }, @INC); - do '/eval_do' or die $@; -} - -{ - package Eval::WithLexicals::Util; - - use B qw(svref_2object); - - sub capture_list { - my $pad_capture = \&Eval::WithLexicals::Cage::pad_capture; - my @names = grep $_ ne '&', map $_->PV, grep $_->isa('B::PV'), - svref_2object($pad_capture)->OUTSIDE->PADLIST->ARRAYelt(0)->ARRAY; - $Eval::WithLexicals::current_code .= - '+{ '.join(', ', map "'$_' => \\$_", @names).' };' - ."\n}\n}\n1;\n"; - } -} +with 'Eval::WithLexicals::Role::Eval'; +with 'Eval::WithLexicals::Role::PreludeEachTime'; =head1 NAME diff --git a/lib/Eval/WithLexicals/PersistHints.pm b/lib/Eval/WithLexicals/PersistHints.pm new file mode 100644 index 0000000..4e7f726 --- /dev/null +++ b/lib/Eval/WithLexicals/PersistHints.pm @@ -0,0 +1,8 @@ +# XXX: Calls for traits really; there's not a MooX::... (yet) +package Eval::WithLexicals::PersistHints; +use Moo; + +with 'Eval::WithLexicals::Role::Eval'; +with 'Eval::WithLexicals::Role::LexicalHints'; + +1; diff --git a/lib/Eval/WithLexicals/Role/Eval.pm b/lib/Eval/WithLexicals/Role/Eval.pm new file mode 100644 index 0000000..823f2a8 --- /dev/null +++ b/lib/Eval/WithLexicals/Role/Eval.pm @@ -0,0 +1,126 @@ +package Eval::WithLexicals::Role::Eval; +use Moo::Role; +use Sub::Quote; + +has lexicals => (is => 'rw', default => quote_sub q{ {} }); + +{ + my %valid_contexts = map +($_ => 1), qw(list scalar void); + + has context => ( + is => 'rw', default => quote_sub(q{ 'list' }), + isa => sub { + my ($val) = @_; + die "Invalid context type $val - should be list, scalar or void" + unless $valid_contexts{$val}; + }, + ); +} + +has in_package => ( + is => 'rw', default => quote_sub q{ 'Eval::WithLexicals::Scratchpad' } +); + +has prelude => ( + is => 'rw', default => quote_sub q{ 'use strictures 1;' } +); + +sub setup_code { + my ($self) = @_; + + return Sub::Quote::capture_unroll('$_[2]', $self->lexicals, 2); +} + +sub capture_code { + ( qq{ BEGIN { Eval::WithLexicals::Util::capture_list() } } ) +} + +sub eval { + my ($self, $to_eval) = @_; + local *Eval::WithLexicals::Cage::current_line; + local *Eval::WithLexicals::Cage::pad_capture; + local *Eval::WithLexicals::Cage::grab_captures; + + my $package = $self->in_package; + my $setup_code = join '', $self->setup_code; + my $capture_code = join '', $self->capture_code; + + local our $current_code = qq! +${setup_code} +sub Eval::WithLexicals::Cage::current_line { +package ${package}; +#line 1 "(eval)" +${to_eval} +;sub Eval::WithLexicals::Cage::pad_capture { } +${capture_code} +sub Eval::WithLexicals::Cage::grab_captures { + no warnings 'closure'; no strict 'vars'; + package Eval::WithLexicals::VarScope;!; + # rest is appended by Eval::WithLexicals::Util::capture_list, called + # during parsing by the BEGIN block from capture_code. + + $self->_eval_do(\$current_code, $self->lexicals, $to_eval); + $self->_run(\&Eval::WithLexicals::Cage::current_line); +} + +sub _run { + my($self, $code) = @_; + + my @ret; + my $ctx = $self->context; + if ($ctx eq 'list') { + @ret = $code->(); + } elsif ($ctx eq 'scalar') { + $ret[0] = $code->(); + } else { + $code->(); + } + $self->lexicals({ + %{$self->lexicals}, + %{$self->_grab_captures}, + }); + @ret; +} + +sub _grab_captures { + my ($self) = @_; + my $cap = Eval::WithLexicals::Cage::grab_captures(); + foreach my $key (keys %$cap) { + my ($sigil, $name) = $key =~ /^(.)(.+)$/; + my $var_scope_name = $sigil.'Eval::WithLexicals::VarScope::'.$name; + if ($cap->{$key} eq eval "\\${var_scope_name}") { + delete $cap->{$key}; + } + } + $cap; +} + +sub _eval_do { + my ($self, $text_ref, $lexical, $original) = @_; + local @INC = (sub { + if ($_[1] eq '/eval_do') { + open my $fh, '<', $text_ref; + $fh; + } else { + (); + } + }, @INC); + do '/eval_do' or die $@; +} + +{ + package Eval::WithLexicals::Util; + + use B qw(svref_2object); + + sub capture_list { + my $pad_capture = \&Eval::WithLexicals::Cage::pad_capture; + my @names = grep $_ ne '&', map $_->PV, grep $_->isa('B::PV'), + svref_2object($pad_capture)->OUTSIDE->PADLIST->ARRAYelt(0)->ARRAY; + $Eval::WithLexicals::Role::Eval::current_code .= + '+{ '.join(', ', map "'$_' => \\$_", @names).' };' + ."\n}\n}\n1;\n"; + } +} + +1; diff --git a/lib/Eval/WithLexicals/Role/LexicalHints.pm b/lib/Eval/WithLexicals/Role/LexicalHints.pm new file mode 100644 index 0000000..8acc369 --- /dev/null +++ b/lib/Eval/WithLexicals/Role/LexicalHints.pm @@ -0,0 +1,72 @@ +package Eval::WithLexicals::Role::LexicalHints; +use Moo::Role; + +our($hints, %hints); + +has first_eval => ( + is => 'rw', + default => sub { 1 }, +); + +has hints => ( + is => 'rw', + default => sub { {} }, +); + +around eval => sub { + my $orig = shift; + my($self) = @_; + + local *Eval::WithLexicals::Cage::capture_hints; + local $Eval::WithLexicals::Cage::hints = { %{$self->hints} }; + + my @ret = $orig->(@_); + + $self->hints({ Eval::WithLexicals::Cage::capture_hints() }); + + @ret; +}; + +# XXX: Sub::Quote::capture_unroll without 'my' +use B(); +sub _capture_unroll_global { + my ($from, $captures, $indent) = @_; + join( + '', + map { + /^([\@\%\$])/ + or die "capture key should start with \@, \% or \$: $_"; + (' ' x $indent).qq{${_} = ${1}{${from}->{${\B::perlstring $_}}};\n}; + } keys %$captures + ); +} + +around setup_code => sub { + my $orig = shift; + my($self) = @_; + # Only run the prelude on the first eval, hints will be set after + # that. + if($self->first_eval) { + $self->first_eval(0); + return $self->prelude, $orig->(@_); + } else { + # Seems we can't use the technique of passing via @_ for code in a BEGIN block + return q[ BEGIN { ], _capture_unroll_global('$Eval::WithLexicals::Cage::hints', $self->hints, 2), q[ } ], + $orig->(@_); + } +}; + +around capture_code => sub { + my $orig = shift; + my($self) = @_; + + ( q{ sub Eval::WithLexicals::Cage::capture_hints { + no warnings 'closure'; + my($hints, %hints); + BEGIN { $hints = $^H; %hints = %^H; } + return q{$^H} => \$hints, q{%^H} => \%hints; + } }, + $orig->(@_) ) +}; + +1; diff --git a/lib/Eval/WithLexicals/Role/PreludeEachTime.pm b/lib/Eval/WithLexicals/Role/PreludeEachTime.pm new file mode 100644 index 0000000..4f5981e --- /dev/null +++ b/lib/Eval/WithLexicals/Role/PreludeEachTime.pm @@ -0,0 +1,10 @@ +package Eval::WithLexicals::Role::PreludeEachTime; +use Moo::Role; + +around setup_code => sub { + my $orig = shift; + my($self) = @_; + ($self->prelude, $orig->(@_)); +}; + +1; diff --git a/t/hints.t b/t/hints.t new file mode 100644 index 0000000..a885dd4 --- /dev/null +++ b/t/hints.t @@ -0,0 +1,51 @@ +use strictures 1; +# Find the hint value that 'use strictures 1' sets on this perl. +my $strictures_hints; +BEGIN { $strictures_hints = $^H } + +use Test::More; +use Eval::WithLexicals::PersistHints; + +my $eval = Eval::WithLexicals::PersistHints->new(prelude => ''); + +is_deeply( + [ $eval->eval('$x = 1') ], + [ 1 ], + 'Basic non-strict eval ok' +); + +is_deeply( + $eval->lexicals, { }, + 'Lexical not stored' +); + +$eval->eval('use strictures 1'); + +{ + local $SIG{__WARN__} = sub { }; + + ok !eval { $eval->eval('$x') }, 'Unable to use undeclared variable'; + like $@, qr/requires explicit package/, 'Correct message in $@'; +} + +is_deeply( + $eval->hints->{q{$^H}}, \$strictures_hints, + 'Hints are set per strictures' +); + +is_deeply( + $eval->lexicals, { }, + 'Lexical not stored' +); + +# Assumption about perl internals: sort pragma will set a key in %^H. + +$eval->eval(q{ { use sort 'stable' } }), +ok !exists $eval->hints->{q{%^H}}->{sort}, + "Lexical pragma used below main scope not captured"; + +$eval->eval(q{ use sort 'stable' }), +ok exists $eval->hints->{q{%^H}}->{sort}, + "Lexical pragma captured"; + +done_testing;