—#
# 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
package
FP::Repl::WithRepl;
use
strict;
use
warnings;
#-- 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) }
}
}
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};
$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