# # Copyright (c) 2015-2020 Christian Jaeger, copying@christianjaeger.ch # # This is free software, offered under either the same terms as perl 5 # or the terms of the Artistic License version 2 or the terms of the # MIT License (Expat version). See the file COPYING.md that came # bundled with this file. # =head1 NAME FP::Repl::WithRepl =head1 SYNOPSIS use FP::Repl::WithRepl; withrepl { die "foo"; }; # shows the exception, then runs a repl # within the exception context push_withrepl (0); # turn on using a repl globally, but storing the # previous handler on a stack; the argument says # how many levels from the current one to go back # for the search of 'eval' (the WORKAROUND, see # below) WithRepl_eval ...; # like `eval` but transparent for WithRepl # handlers (takes an optional package argument, by # default the caller's package is used) my ($v,$e,$is_error) = WithRepl_eval_e("code",$maybe_package); # unlike WithRepl_eval and eval, this safely # returns the result of the given code, or $e == $@ # and $is_error == 1 in case of an exception/error. pop_withrepl; # restore the handler that was pushed last. =head1 DESCRIPTION Sets `$SIG{__DIE__}` to a wrapper that shows the exception then calls a repl from L. This means, when getting an exception, instead of terminating the program (with a message), you get a chance to inspect the program state interactively. Note that it currently employs a WORKAROUND to check from within the sig handler whether there's a new `(eval)` frame on the stack between the point of the handler call and the point of the handler installation (or n frames back from there, as per the argument to `push_withrepl`). =head1 NOTE This is alpha software! Read the status section in the package README or on the L. =cut package FP::Repl::WithRepl; use strict; use warnings; use warnings FATAL => 'uninitialized'; #-- moved up here before any lexicals to avoid their exposure-- # Wrapping `eval` calls with a special frame # (`FP::Repl::WithRepl::WithRepl_eval`) that the handler can test for: sub WithRepl_eval (&;$) { # my ($arg, $maybe_package) = @_; if (ref $_[0]) { @_ == 1 or fp_croak_arity(1); my ($arg) = @_; eval { &$arg() } } else { eval do { @_ == 1 or @_ == 2 or fp_croak_arity("1 or 2"); my ($arg, $maybe_package) = @_; my $package = $maybe_package // caller; "package $package; $arg" } } } sub WithRepl_eval_e { @_ >= 1 and @_ <= 3 or fp_croak_arity("1-3"); # my ($arg, $maybe_package, $wantarray) = @_; if (ref $_[0]) { die "WithRepl_eval_e only supports string eval"; } else { my $success = eval do { (@_ >= 1 and @_ <= 3) or fp_croak_arity("1-3"); my ($arg, $maybe_package, $wantarray) = @_; my $package = $maybe_package // caller; my $scalar = $wantarray ? "" : "scalar"; "[ $scalar do { package $package; $arg } ]" }; my ($arg, $maybe_package, $wantarray) = @_; # now can have the lexicals if ($success) { ($wantarray ? $success : $$success[0], $@, '') } else { (undef, $@, 1) } } } use Exporter "import"; our @EXPORT = qw(withrepl push_withrepl pop_withrepl); our @EXPORT_OK = qw(WithRepl_eval WithRepl_eval_e); our %EXPORT_TAGS = (all => [@EXPORT, @EXPORT_OK]); use FP::Repl; use FP::Repl::Stack; use Chj::TEST; use FP::Show; use FP::Carp; # test that 'no' variables are seen (yeah, could do better) TEST { &WithRepl_eval('"Hello $arg"') } (); TEST { my ($res, $e, $is_err) = WithRepl_eval_e('"Hello $arg"'); $is_err } 1; TEST { my ($res, $e, $is_err) = WithRepl_eval_e('[ $res ]'); $is_err } 1; TEST { my ($res, $e, $is_err) = WithRepl_eval_e q{ my @a = (qw(a b c)); @a }, "foo"; $res } 3; TEST { my ($res, $e, $is_err) = WithRepl_eval_e q{ my @a = (qw(a b c)); @a }, "foo", 1; $res } [qw(a b c)]; # PROBLEM: even exceptions within contexts that catch exceptions # (i.e. `eval { }`) are invoking a repl, unless we use a workaround. # XXX this will be worrysome slow, and on top of that slower for # bigger stack depths, easily turning algorithms into O(n^2)! Needs a # solution in XS. sub current_user_frame { @_ == 1 or fp_croak_arity 1; my ($skip) = @_; if ($skip) { $skip >= 0 or die "expecting maybe(natural0), got '$skip'"; } my @v; my $i = 0; while ((@v) = caller($i++)) { if ($v[0] ne "FP::Repl::WithRepl") { if ($skip) { unless ((@v) = caller($i + $skip)) { die "skip value goes beyond the end of the stack"; } } return FP::Repl::StackFrame->new(undef, @v); } } die "???" } # have_eval_since_frame: is ignoring eval from repl. Uh, so hacky. But # otherwise how to enable WithRepl from within a repl? With a special # repl command? But even when previously the handler was enabled, a # new repl should never be disabling it. (It should not change the # handler, just change the catch point. But other exception catchers # should change the haandler, but don't, which is the reason we need # to analyze here.) our $debug = 0; sub have_eval_since_frame { @_ == 1 or fp_croak_arity 1; my ($startframe) = @_; my @v; my $i = 1; SKIP: { while ((@v) = caller $i++) { last SKIP if ($v[0] ne "FP::Repl::WithRepl"); } die "???" } do { my $f = FP::Repl::StackFrame->new(undef, @v); if ($f->equal($startframe)) { warn "reached startframe, thus return false" if $debug; return '' } elsif ($f->subroutine eq "(eval)") { if ((@v) = caller $i++) { my $f = FP::Repl::StackFrame->new(undef, @v); my $sub = $f->subroutine; if ($sub =~ /::WithRepl_eval(?:_e)?\z/) { warn "(ignore eval since it's from a WithRepl_eval)" if $debug; } elsif ($sub =~ /::BEGIN\z/) { # (why does BEGIN use eval?) warn "(ignore eval since it's from a BEGIN)" if $debug; } else { warn "GOT eval (standalone)" if $debug; return 1 } } else { warn "GOT eval right at end of stack" if $debug; return 1 } } } while ((@v) = caller $i++); warn "couldn't find orig frame!" if $debug; # not even tail-calling should be able to do that, unless, not # local'ized, hm XXX non-popped handler. 0 } sub handler_for { @_ == 2 or fp_croak_arity 2; my ($startframe, $orig_handler) = @_; bless sub { my ($e) = @_; # to show local errors with backtrace: # require Chj::Backtrace; import Chj::Backtrace; if (have_eval_since_frame $startframe) { #$SIG{__DIE__} = $orig_handler; # ^ helps against the loop but makes the push_withrepl # one-shot, of course #goto &{$orig_handler // sub { die $_[0] }} nah, try: if (defined $orig_handler) { #goto $orig_handler # ^ just doesn't work, seems to undo the looping # protection. so..: -- XX test goto &$orig_handler &$orig_handler($e) } else { #warn "no orig_handler, returning"; return } } else { my $err = $FP::Repl::Repl::maybe_output // *STDERR{IO}; print $err "Exception: " . show($e) . "\n"; # then what to do upon exiting it? return the value of the # repl? XX repl needs new feature, a "quit this context # with this value". Although not helping anyway since Perl # can't be made to avoid leaving the exception context. push_withrepl(0); # XX correct? Argument? repl(skip => 1) } }, "FP::Repl::WithRepl::Handler" # just to mark, for Chj::Backtrace ugh } sub handler { @_ == 1 or fp_croak_arity 1; my ($skip) = @_; handler_for(current_user_frame($skip), $SIG{__DIE__}) } sub withrepl (&) { local $SIG{__DIE__} = handler(0); &{ $_[0] }() } TEST { withrepl { 1 + 2 } } 3; TEST { [withrepl { "hello", "world" }] } ['hello', 'world']; our @stack; sub push_withrepl { @_ == 1 or fp_croak_arity 1; my ($skip) = @_; push @stack, $SIG{__DIE__}; $SIG{__DIE__} = handler($skip); } sub pop_withrepl { @_ == 0 or fp_croak_arity 0; $SIG{__DIE__} = pop @stack; } 1