The Perl and Raku Conference 2025: Greenville, South Carolina - June 27-29 Learn more

#
# 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<FP::Repl>. 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<website|http://functional-perl.org/>.
=cut
use strict;
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]);
# 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