Add persistent hints
David Leadbeater [Tue, 11 Jan 2011 21:19:38 +0000 (21:19 +0000)]
(docs still to write)

lib/Eval/WithLexicals.pm
lib/Eval/WithLexicals/PersistHints.pm [new file with mode: 0644]
lib/Eval/WithLexicals/Role/Eval.pm [new file with mode: 0644]
lib/Eval/WithLexicals/Role/LexicalHints.pm [new file with mode: 0644]
lib/Eval/WithLexicals/Role/PreludeEachTime.pm [new file with mode: 0644]
t/hints.t [new file with mode: 0644]

index fab265c..7028702 100644 (file)
 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 (file)
index 0000000..4e7f726
--- /dev/null
@@ -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 (file)
index 0000000..823f2a8
--- /dev/null
@@ -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 (file)
index 0000000..8acc369
--- /dev/null
@@ -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 (file)
index 0000000..4f5981e
--- /dev/null
@@ -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 (file)
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;