#
# Copyright (c) 2004-2019 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::Repl - read-eval-print loop

=head1 SYNOPSIS

 my $repl= new FP::Repl::Repl;
 $repl->set_prompt("foo> ");
 # ^ if left undefined, "$package$perhapslevel> " is used
 $repl->set_historypath("somefile"); # default is ~/.fp-repl_history
 $repl->set_env_PATH ($safe_PATH); # default in taint mode is
   # '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
   # $ENV{PATH} otherwise.
 $repl->run;
 # or $repl->run($skip)  to skip $skip levels

=head1 DESCRIPTION

Enters an interactive read-eval-print loop.  Term::ReadLine with
history is active.  The loop can be exited by typing ctl-d.  Entering
nothing re-evaluates the last entry.  Some autocompletion exists.

When the entered line starts with ':' or ',' (both characters are
equivalent), then it is interpreted as containing special commands or
modes. Enter ':h' or ':help' or ':?' (or the equivalents starting with
the comma like ',?', from now on whenever the text days ':' you can
also use the comma) to get a help text including the currently active
settings.

If the 'Maybe_keepResultIn' field is set to a string, the scalar with the
given mae is set to either an array holding all the result values (in
:l mode) or the result value (in :1 mode).

By default, in the :d and :s modes the results of a calculation are
carried over to the next entry in $VAR1 etc. as shown by the display
of the result. Those are lexical variables.

This does not turn on the Perl debugger, hence programs are not slowed
down.

=head1 FEATURES

Read the help text that is displayed by entering ":h", ",h", ":?" or
",?" in the repl.

=head1 TODO

 - 'A::Class-> ' method completion
 - for '$Foo ->bar<tab>' completion, if $Foo contains a valid class
   name, use it
 - maybe '$ans->[1]->' method completion

=head1 IDEAS

 - should stdout and stderr of the evaluation context go to the pager,
   too?
 - maybe handle ->SUPER:: completion?
 - differ between emptylistofcompletions (no &subs for example) (empty
   list) and no sigil there so dunno-how-to-complete (undef?,
   exception?, ??).
 - make package lexicals accessible when entering a package with :p

=head1 BUGS

Completion:

 - $ does not filter out scalars only, since perl is not able to do so
 - % and * make completion stop working unless you put a space after
   those sigils. (@ and & work as they should)
 - keep the last 10 or so completion lists, and use those in the ->
   case if the var's type could not be determined.

:V breaks view of :e and similar (shows CODE..)


=head1 SEE ALSO

L<FP::Repl>: easy wrapper

=head1 NOTE

This is alpha software! Read the package README.

=cut


package FP::Repl::Repl;

use strict; use warnings; use warnings FATAL => 'uninitialized';

# Copy from FP::Repl::WithRepl, to prevent circular dependency.  This has
# to be at the top before any lexicals are defined! so that lexicals
# from this module are not active in the eval'ed code.

sub WithRepl_eval (&;$) {
    my ($arg, $maybe_package)=@_;
    if (ref $arg) {
        eval { &$arg() }
    } else {
        my $package= $maybe_package // caller;
        eval "package $package; $arg"
    }
}

use Chj::Class::methodnames;
use Chj::xoutpipe();
use Chj::xtmpfile;
use Chj::xperlfunc qw(xexec);
use Chj::xopen qw(fh_to_fh perhaps_xopen_read);
use POSIX;
use Chj::xhome qw(xeffectiveuserhome);
use Chj::singlequote 'singlequote';
use FP::HashSet qw(hashset_union);
use FP::Hash qw(hash_xref);
use FP::Repl::StackPlus;
use FP::Lazy;

sub maybe_tty {
    my $path= "/dev/tty";
    if (open my $fh, "+>", $path) {
        $fh
    } else {
        warn "opening '$path': $!";
        undef
    }
}


sub xone_nonwhitespace {
    my ($str)=@_;
    $str=~ /^\s*(\S+)\s*\z/s
        or die "exactly one non-quoted argument must be given";
    $1
}


my $HOME=xeffectiveuserhome;
our $maybe_historypath= "$HOME/.fp-repl_history";
our $maybe_settingspath= "$HOME/.fp-repl_settings";
our $maxHistLen= 100;
our $doCatchINT= 1;
our $doRepeatWhenEmpty= 1; 
our $doKeepResultsInVARX= 1;
our $pager= $ENV{PAGER} || "less";
our $mode_context= 'l';
our $mode_formatter= 'd';
our $mode_viewer= 'a';
our $mode_lexical_persistence= 'X';
our $maybe_env_path= '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin';

use Chj::Class::Array -fields=>
  -publica=> (
              'Maybe_historypath', # undef=none, but a default is set
              'Maybe_settingspath', # undef=none, but a default is set
              'MaxHistLen',
              'Maybe_prompt', # undef= build fresh one from package&level
              'Maybe_package', # undef= use caller's package
              'DoCatchINT',
              'DoRepeatWhenEmpty',
              'Maybe_keepResultIn',
              'DoKeepResultsInVARX',
              'Pager',
              'Mode_context', # char
              'Mode_formatter', # char
              'Mode_viewer', # char
              'Mode_lexical_persistence', # char
              'Maybe_input', # fh
              'Maybe_output', # fh
              'Maybe_env_PATH', # maybe string
             );

sub new {
    my $class=shift;
    my $self= $class->SUPER::new;
    $$self[Maybe_historypath]= $maybe_historypath;
    $$self[Maybe_settingspath]= $maybe_settingspath;
    $$self[MaxHistLen]= $maxHistLen;
    $$self[DoCatchINT]= $doCatchINT;
    $$self[DoRepeatWhenEmpty]= $doRepeatWhenEmpty;
    $$self[DoKeepResultsInVARX]= $doKeepResultsInVARX;
    $$self[Pager]= $pager;
    $$self[Mode_context]= $mode_context;
    $$self[Mode_formatter]= $mode_formatter;
    $$self[Mode_viewer]= $mode_viewer;
    $$self[Mode_lexical_persistence]= $mode_lexical_persistence;
    $$self[Maybe_env_PATH]= $maybe_env_path
        if ${^TAINT};
    $self
}

sub use_lexical_persistence {
    my $self=shift;
    hash_xref(+{m=>1, M=>1, x=>0, X=>0}, $self->mode_lexical_persistence)
}
sub use_strict_vars {
    my $self=shift;
    hash_xref(+{m=>1, M=>0, x=>1, X=>0}, $self->mode_lexical_persistence)
}

my $settings_version= "v2";
my $settings_fields=
  [
   # these should remain caller dependent:
   #maxHistLen
   #doCatchINT
   #doRepeatWhenEmpty
   #maybe_keepResultIn
   #doKeepResultsInVARX
   #pager
   qw(mode_context
      mode_formatter
      mode_viewer
      mode_lexical_persistence)
   ];

sub possibly_save_settings {
    my $self=shift;
    if (my $path= $self->maybe_settingspath) {
        my $f= xtmpfile $path;
        $f->xprint (join("\0",
                         $settings_version,
                         map {
                             $self->$_
                         }
                         @$settings_fields));
        $f->xclose;
        $f->xputback(0600);
    }
}

sub possibly_restore_settings {
    my $self=shift;
    if (my $path= $self->maybe_settingspath) {
        if (my ($f)= perhaps_xopen_read ($path)) {
            my @v= split /\0/, $f->xcontent;
            $f->xclose;
            if (shift (@v) eq $settings_version) {
                for (my $i=0; $i< @$settings_fields; $i++) {
                    my $method= "set_".$$settings_fields[$i];
                    $self->$method($v[$i]);
                }
            } else {
                warn "note: not reading settings of other version from '$path'";
            }
        }
    }
}

sub saving ($$) {
    my ($self,$proc)=@_;
    sub {
        &$proc(@_);
        $self->possibly_save_settings;
    }
}

# (move to some lib?)
sub splitpackage {
    my ($package)=@_; # may be partial.
    if ($package=~ /(.*)::(.*)/s) {
        ($1,$2)
    } else {
        ("",$package)
    }
}

my $PACKAGE= qr/\w+(?:::\w+)*/;

use FP::Repl::corefuncs();
our @builtins= FP::Repl::corefuncs;

# whether to use Data::Dumper in perl mode
our $Dumper_Useperl= 0;

sub __signalhandler { die "SIGINT\n" }

our $term; # local'ized but old value is reused if present.

our $current_history; # local'ized; array(s).


sub print_help {
    my $self= shift;
    my ($out)=@_;
    my $selection= sub {
        my ($meth,$val)=@_;
        my $method= "mode_$meth";
        $self->$method eq $val ? "->" : "  "
    };
    my $L= &$selection(context=> '1');
    my $l= &$selection(context=> 'l');
    my $p= &$selection(formatter=> 'p');
    my $s= &$selection(formatter=> 's');
    my $d= &$selection(formatter=> 'd');
    my $V= &$selection(viewer=> 'V');
    my $v= &$selection(viewer=> 'v');
    my $a= &$selection(viewer=> 'a');
    my $m= &$selection(lexical_persistence=> 'm');
    my $M= &$selection(lexical_persistence=> 'M');
    my $x= &$selection(lexical_persistence=> 'x');
    my $X= &$selection(lexical_persistence=> 'X');
    print $out qq{Repl help:
If a command line starts with a ':' or ',', then the remainder of the
line is interpreted as follows:

  package \$package    use \$package as new compilation package
  DIGITS              shorthand for 'f n', see below
  -                   same as 'f n' where n is the current frameno - 1
  +                   same as 'f n' where n is the current frameno + 1
                       (both are the same as 'f' if reaching the end)
  CMD args...         one-time command
  MODES code...       change some modes then evaluate code

CMD is one of:
   e [n]  print lexical environment at level n (default: 0)
           Note: currently this does *not* show the lexicals that were
           persisted when enabling m or M mode.
   b|bt   print back trace
   f [n]  move to stack frame number n (at start: 0)
           without n, shows the current frame again
   y [n]  alias for f
   q      quit the process (by calling `exit`)

MODES are a combination of these characters, which change the
previously used mode (indicated on the left):
  context:
$L 1  use scalar context
$l l  use list context (default)
  formatter:
$p p  print stringification
$s s  show from FP::Show (experimental, does not show data sharing)
$d d  Data::Dumper (default)
  viewer:
$V V  no pager
$v v  pipe to pager ($$self[Pager])
$a a  pipe to 'less --quit-if-one-screen --no-init' (default)
  lexical persistence:
   (Persisting lexicals means to carry over variables introduced with
   "my" into subsequent entries in the same repl. It prevents their
   values from being deallocated until new values are assigned or a
   new lexical with the same name is introduced. Currently these
   lexicals are carried over even when moving to another frame or
   package.)
$m m  persist lexicals, use strict 'vars'
$M M  persist lexicals, no strict 'vars'
$x x  do not persist lexicals, use strict 'vars'
$X X  do not persist lexicals, no strict 'vars' (default)

Other features:
  \$FP::Repl::Repl::args   is an array holding the arguments of the last subroutine call
                     that led to the currently selected frame
  \$FP::Repl::Repl::argsn  is an array holding the arguments of the subroutine call
                     that *leaves* the currently selected frame
};
}


sub formatter {
    my $self=shift;
    my ($terse)=@_; # true for :e viewing
    my $mode= $self->mode_formatter;
    $mode= "d" if ($terse and $mode eq "p");
    hash_xref
      (+{
         p=> sub {
             (
              join "",
              map {
                  (defined $_ ? $_ : 'undef'). "\n"
              } @_
             )
         },
         s=> sub {
             require FP::Show;
             my $z=1;
             (
              join "",
              map {
                  my $VARX= ($$self[DoKeepResultsInVARX] and not $terse) ?
                    '$VAR'.$z++.' = '
                      : '';
                  $VARX . FP::Show::show($_). ";\n"
              } @_
             )
         },
         d=> sub {
             my @v= @_; # to survive into
             # WithRepl_eval below

             require Data::Dumper;
             my $res;
             WithRepl_eval {
                 local $Data::Dumper::Sortkeys= 1;
                 local $Data::Dumper::Terse= $terse;
                 local $Data::Dumper::Useperl= $Dumper_Useperl;
                 $res= Data::Dumper::Dumper(@v);
                 1
             } || do {
                 warn "Data::Dumper: $@";
             };
             $res
         }
        },
       $mode);
}

sub viewers {
    my $self=shift;
    my ($OUTPUT,$ERROR)=@_;
    my $port_pager_with_options= sub {
        my ($maybe_pager, @options)=@_;
        sub {
            my ($printto)=@_;

            local $SIG{PIPE}="IGNORE";

            my $pagercmd= $maybe_pager // $self->pager;

            eval {
                # XX this now means that no options
                # can be passed in $ENV{PAGER} !
                # (stupid Perl btw). Ok hard code
                # 'less' instead perhaps!
                my $o= Chj::xoutpipe
                  (sub {
                       # set stdout and stderr in case they are
                       # redirected (stdin is the pipe)
                       my $out= fh_to_fh ($OUTPUT);
                       $out->xdup2(1);
                       $out->xdup2(2);
                       $ENV{PATH}= $self->maybe_env_PATH
                         if defined $self->maybe_env_PATH;
                       xexec $pagercmd, @options
                   });
                &$printto ($o);
                $o->xfinish;
                1
            } || do {
                my $e= $@;
                unless ($e=~ /broken pipe/i) {
                    print $ERROR "error piping to pager ".
                      "$pagercmd: $e\n"
                        or die $!;
                }
            };
        }
    };

    my $string_pager_with_options= sub {
        my $port_pager= &$port_pager_with_options (@_);
        sub {
            my ($v)=@_;
            &$port_pager (sub {
                              my ($o)=@_;
                              $o->xprint($v);
                          });
        }
    };

    my $choosepager= sub {
        my ($pager_with_options)= @_;
        hash_xref
          (+{
             V=> sub {
                 print $OUTPUT $_[0]
                   or die "print: $!";
             },
             v=> &$pager_with_options(),
             a=> &$pager_with_options
             (qw(less --quit-if-one-screen --no-init)),
            },
           $self->mode_viewer);
    };

    my $pager= sub {
        my ($pager_with_options)= @_;
        sub {
            my ($v)=@_;
            &$choosepager ($pager_with_options)->($v);
        }
    };

    (&$pager ($port_pager_with_options),
     &$pager ($string_pager_with_options))
}


our $use_warnings= q{use warnings; use warnings FATAL => 'uninitialized';};

sub eval_code {
    my $self= shift;
    @_==5 or die "wrong number of arguments";
    my ($code, $in_package, $maybe_lexicals, $maybe_kept_results,
        $maybe_lexical_persistence)=@_;

    # merge with previous results, if any
    my $maybe_kept_results_hash= sub {
        return unless $maybe_kept_results;
        my %r;
        for (my $i=0; $i<@$maybe_kept_results; $i++) {
            $r{'$VAR'.($i+1)}= \ ($$maybe_kept_results[$i]);
        }
        \%r
    };
    $maybe_lexicals=
      ($maybe_lexicals && $maybe_kept_results) ?
        hashset_union ($maybe_lexicals, &$maybe_kept_results_hash)
          : ($maybe_lexicals // &$maybe_kept_results_hash);

    my $use_method_signatures=
      $Method::Signatures::VERSION ? "use Method::Signatures" : "";
    my $use_functional_parameters_=
      $Function::Parameters::VERSION ? "use Function::Parameters ':strict'" : "";
    my $use_tail=
      $Sub::Call::Tail::VERSION ? "use Sub::Call::Tail" : "";

    my $prelude=
      "package ".&$in_package().";".
      "use strict; ".
       ($self->use_strict_vars ? "" : "no strict 'vars'; ").
       "$use_warnings; ".
       "$use_method_signatures; $use_functional_parameters_; $use_tail; ";

    if (my $lp= $maybe_lexical_persistence) {
        my $allcode=
          $prelude.
          $code;
        if (defined $maybe_lexicals) {
            $lp->lexicals(hashset_union($lp->lexicals, $maybe_lexicals))
        }
        my $context= wantarray ? "list" : "scalar";
        $lp->context($context);
        WithRepl_eval { $lp->eval($allcode) }
    } else {
        my @v= sort keys %$maybe_lexicals
          if defined $maybe_lexicals;
        my $allcode=
          $prelude.
          (@v ? 'my ('.join(", ", @v).'); ' : '') .
          'sub {'.
              $code."\n".
          '}';
        my $thunk= &WithRepl_eval($allcode)
          // return;
        PadWalker::set_closed_over ($thunk, $maybe_lexicals)
            if defined $maybe_lexicals;
        WithRepl_eval { &$thunk() }
    }
}


sub _completion_function {
    my ($attribs, $package, $lexicals)=@_;
    sub {
        my ($text, $line, $start, $end) = @_;
        my $part= substr($line,0,$end);

        #reset to the default before deciding upon it:
        $attribs->{completion_append_character}=" ";

        my @matches= do {
            # arrow completion:
            my ($pre,$varnam,$brace,$alreadywritten);
            if (($pre,$varnam,$brace,$alreadywritten)=
                $part=~ /(.*)\$(\w+)\s*->\s*([{\[]\s*)?(\w*)\z/s
                or
                ($pre,$varnam,$brace,$alreadywritten)=
                $part=~ /(.*\$)\$(\w+)(?:\s+|(?:\s*([{\[]\s*)(\w*)))\z/s) {
                # need to know the class of that thing
                no strict 'refs';
                my $r;
                if (my $val=
                    (
                     # try to get the value, or at least the package.

                     do {
                         if (my $ref= $$lexicals{'$'.$varnam}) {
                             $$ref
                         } else {
                             undef
                         }
                     }

                     or $ { $package."::".$varnam }

                     or do {
                         # (if I could run code side-effect free... or
                         # compile-only and disassemble....)  Try to
                         # parse the perl myself
                         if ($part=~ /.* # force latest possible match (ok?)
                                      (?:^|;)\s*
                                      (?:(?:my|our)\s+)?
                                      # ^ optional for no 'use strict'
                                      \$$varnam
                                      \s*=\s*
                                      (?:new\w*\s+($PACKAGE)
                                      |($PACKAGE)\s*->\s*new)
                                     /sx) {
                             $r=$1;
                             1
                         } else {
                             0
                         }
                     })) {
                    #warn "got value from \$$varnam";
                    if ($r||=ref($val)) {
                        if ($r eq 'HASH'
                            or ($brace
                                and UNIVERSAL::isa($val, 'HASH'))) {
                            # Could also check `$val->isa('HASH')` if
                            # we wanted to run isa overloads, but
                            # would we want to do that?

                            #("{")
                            #("{foo}","{bar}")
                            if ($brace) {
                                map {"$_}"} keys %$val
                            } else {
                                map {"{$_}"} keys %$val
                            }
                        }
                        elsif ($r eq 'ARRAY'
                               or ($brace
                                   and UNIVERSAL::isa($val, 'ARRAY'))) {
                            # ^ not sure this works here; see commit messages
                            ("[")
                        }
                        elsif ($r eq 'CODE') {
                            ("(")
                        }
                        elsif ($r eq 'SCALAR') {
                            ("SCALAR")##
                        }
                        elsif ($r eq 'IO') {
                            ("IO")##
                        }
                        elsif ($r eq 'GLOB') {
                            ("GLOB")##
                        }
                        else {
                            # object
                            my @a= methodnames($r);
                            grep {
                                # (no need to check for matching the
                                # already-written part of the string
                                # here (with something like
                                # /^\Q$alreadywritten\E/), why? Maybe
                                # $attribs->{completion_word} was
                                # deleted or?)

                                # exclude some of the possible methodnames:
                                # - all-uppercase when characters are contained.
                                not(/[A-Z]/ and uc($_) eq $_)
                            } @a
                        }
                    } else {
                        ()
                    }
                } else {
                    #warn "no value von \$$varnam";
                    ()
                }
            } elsif ($part=~ tr/"/"/ % 2) {
                # odd number of quotes means we are inside
                ()
            } elsif ($part=~ tr/'/'/ % 2) {
                # odd number of quotes means we are inside
                ()
            } elsif ($part=~ /(^|.)\s*(${PACKAGE}(?:::)?)\z/s
                     or
                     $part=~ /([\$\@\%\*\&])
                              \s*
                              (${PACKAGE}(?:::)?|)
                              # ^ accept the empty string
                              \z/sx) {
                # namespace completion
                my ($sigil,$partialpackage)=($1,$2);

                no strict 'refs';

                my ($upperpackage,$localpart)= splitpackage($partialpackage);
                #warn "upperpackage='$upperpackage', localpart='$localpart'\n";

                # if upperpackage is empty, it might also be a
                # non-fully qualified, i.e. local, partial identifier.
                
                my $globentry=
                  ($sigil and
                   +{
                     '$'=>'SCALAR',
                     '@'=>'ARRAY',
                     '%'=>'HASH',
                     # ^ (problem with readline library, with a space
                     # after % it works too; need better completion
                     # function than the one from gnu readline?) 
                     # (years later: what was this?)
                     '*'=>'SCALAR',
                     # ^ really 'GLOB', but that would make it
                     # invisible. SCALAR matches everything, which is
                     # what we want.
                     '&'=>'CODE'
                    }->{$sigil});
                #print $ERROR "<$globentry>";

                my $symbols_for_package= sub {
                    my ($package)=@_;
                    grep {
                        # only show 'usable' ones.
                        /^\w+(?:::)?\z/
                    } do {
                        if ($globentry) {
                            #print $ERROR ".$globentry.";
                            grep {
                                (/::\z/
                                 # either it's a namespace which we
                                 # want to see regardless of type, or:
                                 # type exists
                                 or
                                 *{ $package."::".$_ }{$globentry})
                            } keys %{ $package."::" }
                        } else {
                            keys %{ $package."::" }
                        }
                    }
                };
                my @a=
                  ($symbols_for_package->($upperpackage),

                   length($upperpackage) ?
                   () :
                   ($symbols_for_package->($package),
                    ($globentry ? () : @builtins),
                    # and lexicals:
                    map { /^\Q$sigil\E(.*)/s ? $1 : () } sort keys %$lexicals
                   ));

                #print Data::Dumper::Dumper(\@a);

                # Now, if it ends in ::, or even generally, care about
                # it not appending space on completion:
                $attribs->{completion_append_character}="";

                (
                 map {
                     if (/::\z/) {
                         $_
                     } else {
                         "$_ "
                     }
                 }
                 (length ($upperpackage) ?
                  map {
                      $upperpackage."::$_"
                  } @a
                  : @a)
                )
            } else {
                ()
            }
        };
        if (@matches) {
            #print $ERROR "<".join(",",@matches).">";

            $attribs->{completion_word}= \@matches;
            # (no sorting necessary)

            return
              $term->completion_matches
                ($text,
                 $attribs->{list_completion_function})
        } else {
            # restore defaults.
            $attribs->{completion_append_character}=" ";
            return ()
        }
    }
}

our ($maybe_input, $maybe_output); # dynamic parametrization of
                                   # filehandles

our $repl_level; # maybe number of repl layers above
our $args; # see '$FP::Repl::Repl::args' in help text
our $argsn; # see '$FP::Repl::Repl::argsn' in help text

# TODO: split this monstrosity into pieces.
sub run {
    my ($self, $maybe_skip)=@_;

    my $skip= $maybe_skip // 0;
    my $stack= FP::Repl::StackPlus->get ($skip + 1);

    local $repl_level= ($repl_level // -1) + 1;

    my $frameno= 0;

    my $get_package= sub {
        # (What is $$self[Maybe_package] for? Can set the maybe_prompt
        # independently. Security feature or just overengineering?
        # Ok, remember the ":p" setting; but why not use a lexical
        # within `run`? Ok how long-lived are the repl objects, same
        # duration? Then hm is the only reason for the object to be
        # able to set up things explicitely first? Thus is it ok after
        # all?)
        my $r= $$self[Maybe_package] || $stack->package($frameno);
        $r
    };

    my $oldsigint= $SIG{INT};
    # It seems this is the only way to make signal handlers work in
    # both perl 5.6 and 5.8:
    sigaction SIGINT,
      new POSIX::SigAction __PACKAGE__.'::__signalhandler'
        or die "Error setting SIGALRM handler: $!\n";

    {
        local $SIG{__DIE__};
        require Term::ReadLine;
    }
    # only start one readline instance, do not nest (doing otherwise
    # seems to lead to segfaults). okay?.
    local our $term = $term || new Term::ReadLine 'Repl';
    # This means that the history from nested repls will also show up
    # in the history of the parent repl. Not saved, but within the
    # readline instance. (Correct?)
    # XX: idea: add nesting level to history filename?

    my $attribs= $term->Attribs;

    my ($INPUT, $OUTPUT, $ERROR)= do {
        my $tty= lazy { maybe_tty };
        my $in= $self->maybe_input // $maybe_input // force($tty)
          // $term->IN // *STDIN;
        my $out= $self->maybe_output // $maybe_output // force($tty)
          // $term->OUT // *STDOUT;
        $term->newTTY ($in,$out);
        ($in,$out,$out)
    };
    # carry over input/output to subshells:
    local $maybe_input= $INPUT;
    local $maybe_output= $OUTPUT;

    my $printerror_frameno= sub {
        my $max = $stack->max_frameno;
        print $ERROR "frame number must be between 0..$max",
          (@_ ? ", got @_" : ()), "\n";
    };

    my ($view_with_port, $view_string)= $self->viewers ($OUTPUT,$ERROR);

    {
        my @history;
        local $current_history= \@history;
        # ^ this is what nested repl's will use to restore the history
        # in the $term object
        if (defined $$self[Maybe_historypath]) {
            # clean history of C based object before we re-add the
            # saved one:
            $term->clear_history;
            if (open my $hist, "<", $$self[Maybe_historypath]){
                @history= <$hist>;
                close $hist;
                for (@history){
                    chomp;
                    $term->addhistory($_);
                }
            }
        }
        # do not add input to history automatically (which allows me
        # to do it myself):
        $term->MinLine(undef);

        my $myreadline= sub {
          DO: {
              my $line;
              eval {
                  $line=
                      $term->readline
                      ($$self[Maybe_prompt] //
                       &$get_package()
                       .($repl_level ? " $repl_level":"")
                       .($frameno ? "/$frameno" : "")
                       ."> ");
                  1
              } || do {
                  if (!length ref($@) and $@=~ /^SIGINT\n/s) {
                      print $OUTPUT "\n";
                      redo DO;
                  } else {
                      die $@
                  }
              };
              return $line;
            }
        };

        # for repetitions:
        my $evaluator= sub { }; # noop

        my $maybe_kept_results;

        my $maybe_lexical_persistence;
        my $try_enable_lexical_persistence= sub {
            eval {
                require Eval::WithLexicals;
                $maybe_lexical_persistence= Eval::WithLexicals->new;
                eval { require strictures } or
                  $maybe_lexical_persistence->prelude("");
                1
            } || do {
                print $ERROR "Could not enable lexical persistence: $@";
                0
            }
        };
        &$try_enable_lexical_persistence() if $self->use_lexical_persistence;

      READ: {
            while (1) {

                local $attribs->{attempted_completion_function}=
                  _completion_function ($attribs,
                                        &$get_package,
                                        $stack->perhaps_lexicals($frameno) // {});

                my $input = &$myreadline // last;

                if (length $input) {
                    my ($cmd,$rest)=
                      $input=~ /^ *[:,] *([?+-]|[a-zA-Z]+|\d+)(.*)/s ?
                        ($1,$2)
                          :(undef,$input);

                    if (defined $cmd) {

                        if ($cmd=~ /^\d+\z/) {
                            # hacky way to allow ":5" etc. as ":f 5"
                            $rest= "$cmd $rest";
                            $cmd= "f";
                        }

                        # handle commands
                        eval {

                            my $set_package= sub {
                                # (no package parsing, trust user)
                                $$self[Maybe_package]= xone_nonwhitespace($rest);
                                $rest=""; # XX HACK
                            };

                            my $help= sub { $self->print_help ($OUTPUT) };

                            my $bt= sub {
                                my ($maybe_frameno)=
                                  $rest=~ /^\s*(\d+)?\s*\z/
                                    or die "expecting digits or no argument, got '$cmd'";
                                print $OUTPUT $stack->backtrace ($maybe_frameno
                                                                 // $frameno);
                                $rest=""; # XX HACK; also, really
                                          # silently drop stuff?
                            };

                            my $chooseframe= sub {
                                my ($maybe_frameno)= @_;
                                $rest= ""; # still the hack, right?
                                if (defined $maybe_frameno) {
                                    if ($maybe_frameno <= $stack->max_frameno) {
                                        $frameno= $maybe_frameno
                                    } else {
                                        &$printerror_frameno ($maybe_frameno);
                                        return;
                                    }
                                }
                                
                                # unset any explicit package as
                                # we want to use the one of the
                                # current frame
                                undef $$self[Maybe_package]; # even without frameno? mess

                                # Show the context: (XX same
                                # issue as with :e with overly
                                # long data (need viewer, but
                                # don't really want to, so should
                                # really use shortener)
                                print $OUTPUT $stack->desc($frameno, $self->mode_formatter),"\n";
                            };

                            my $select_frame= sub {
                                my ($maybe_frameno)=
                                  $rest=~ /^\s*(\d+)?\s*\z/
                                    or die "expecting frame number, ".
                                      "an integer, or nothing, got '$cmd'";
                                &$chooseframe ($maybe_frameno)
                            };

                            my %commands=
                                (
                                 h=> $help,
                                 help=> $help,
                                 '?'=> $help,
                                 '-'=> sub {
                                     &$chooseframe (($frameno > 0) ?
                                                    $frameno - 1 : undef)
                                 },
                                 '+'=> sub {
                                     &$chooseframe (($frameno < $stack->max_frameno) ?
                                                    $frameno + 1 : undef)
                                 },
                                 package=> $set_package,
                                 1=> saving ($self, sub { $$self[Mode_context]="1" }),
                                 l=> saving ($self, sub { $$self[Mode_context]="l" }),
                                 p=> saving ($self, sub { $$self[Mode_formatter]="p" }),
                                 s=> saving ($self, sub { $$self[Mode_formatter]="s" }),
                                 d=> saving ($self, sub { $$self[Mode_formatter]="d" }),
                                 V=> saving ($self, sub { $$self[Mode_viewer]="V" }),
                                 v=> saving ($self, sub { $$self[Mode_viewer]="v" }),
                                 a=> saving ($self, sub { $$self[Mode_viewer]="a" }),
                                 m=> saving ($self, sub {
                                                 &$try_enable_lexical_persistence() and
                                                   $$self[Mode_lexical_persistence]= "m"
                                               }),
                                 M=> saving ($self, sub {
                                                 &$try_enable_lexical_persistence() and
                                                   $$self[Mode_lexical_persistence]= "M"
                                               }),
                                 x=> saving ($self, sub {
                                                 undef $maybe_lexical_persistence;
                                                 $$self[Mode_lexical_persistence]= "x"
                                               }),
                                 X=> saving ($self, sub {
                                                 undef $maybe_lexical_persistence;
                                                 $$self[Mode_lexical_persistence]= "X"
                                               }),
                                 e=> sub {
                                     use Data::Dumper;
                                     # XX clean up: don't want i in the regex
                                     my ($maybe_frameno)=
                                         $rest=~ /^i?\s*(\d+)?\s*\z/
                                         or die "expecting digits or no argument, got '$cmd'";
                                     $rest=""; # can't s/// above when expecting value
                                     my $fno= $maybe_frameno // $frameno;
                                     if (my ($lexicals)=
                                         $stack->perhaps_lexicals($fno)) {
                                         &$view_with_port
                                           (sub {
                                                my ($o)=@_;
                                                my $format= $self->formatter(1);
                                                for my $key (sort keys %$lexicals) {
                                                    if ($key=~ /^\$/) {
                                                        $o->xprint
                                                          ("$key = ".
                                                           &$format(${$$lexicals{$key}}));
                                                    } else {
                                                        $o->xprint
                                                          ("\\$key = ".
                                                           &$format($$lexicals{$key}));
                                                    }
                                                }
                                            });
                                     } else {
                                         &$printerror_frameno ($fno);
                                     }
                                 },
                                 f=> $select_frame,
                                 y=> $select_frame,
                                 q=> sub {
                                     # XX change exit code depending
                                     # on how the repl was called?
                                     # Well, at least make it a config
                                     # field?
                                     exit 0
                                 },
                                 bt=> $bt,
                                 b=> $bt,
                                );

                            while (length $cmd) {
                                # XX why am I checking with and without chopping here?
                                if (my $sub= $commands{$cmd}) {
                                    &$sub;
                                    last;
                                } else {
                                    my $subcmd= chop $cmd;
                                    if (my $sub= $commands{$subcmd}) {
                                        &$sub;
                                        last; # XX why last? shouldn't we
                                              # continue with what's left of
                                              # $cmd ?
                                    } else {
                                        print $ERROR "unknown command "
                                          ."or mode: '$subcmd'\n";
                                        last;
                                    }
                                }
                            }

                            1
                        } || do {
                            print $ERROR "$@";
                            redo READ;
                        }
                    }

                    # build up evaluator
                    my $eval= do {
                        my $eval_= sub {
                            $self->eval_code
                              ($rest,
                               $get_package,
                               $stack->perhaps_lexicals($frameno) // {},
                               $maybe_kept_results,
                               $maybe_lexical_persistence)
                        };
                        hash_xref
                          (+{
                             1=> sub {
                                 ([ scalar &$eval_() ], $@)
                             },
                             l=> sub {
                                 ([ &$eval_() ], $@)
                             },
                            },
                           $self->mode_context);
                    };

                    my $format_vals= $self->formatter;

                    $evaluator= sub {
                        my ($results,$error)= do {
                            # make it possible for the code entered in
                            # the repl to access the arguments in the
                            # last call leading to this position by
                            # accessing $FP::Repl::Repl::args :
                            my $getframe= sub {
                                my ($i)=@_;
                                if (defined (my $frame= $stack->frame
                                             ($frameno + $i))) {
                                    $frame->args
                                } else {
                                    "TOP"
                                }
                            };
                            local $argsn = &$getframe(0);
                            local $args = &$getframe(1);
                            &$eval()
                        };

                        &$view_string(do {
                            if (ref $error or $error) {
                                # XX todo: only do can in the case of
                                # ref; but, remove this code anyway,
                                # use FP::Show now?
                                my $err= (UNIVERSAL::can($error,"plain") ?
                                          # error in plaintext; XX:
                                          # change to better
                                          # thought-out protocol?
                                          $error->plain
                                          : "$error");
                                chomp $err;
                                $err."\n"; # no prefix? no safe way to differentiate.
                            } else {
                                if (my $varname= $$self[Maybe_keepResultIn]) {
                                    $varname= &$get_package()."::$varname"
                                      unless $varname=~ /::/;
                                    no strict 'refs';
                                    $$varname= $self->mode_context eq "1" ?
                                      $$results[0] : $results;
                                }
                                &$format_vals(@$results)
                            }
                        });
                        $maybe_kept_results= $results
                          if $$self[DoKeepResultsInVARX];
                    };

                    &$evaluator();

                } elsif ($$self[DoRepeatWhenEmpty]) {
                    &$evaluator();
                } else {
                    next;
                }

                if (length $input and
                    ((!defined $history[-1]) or $history[-1] ne $input)) {
                    push @history,$input;
                    chomp $input;
                    $term->addhistory($input);
                    #splice @history,0,@history-$$self[MaxHistLen] =();
                    if ($$self[MaxHistLen] >= 0){# <-prevent endless loop
                        shift @history while @history>$$self[MaxHistLen];
                    }
                }
            }
        }
        print $OUTPUT "\n";
        if (defined $$self[Maybe_historypath]) {
            eval {
                my $f= xtmpfile $$self[Maybe_historypath];
                $f->xprint("$_\n") for @history;
                $f->xclose;
                $f->xputback(0600);
            };
            if (ref $@ or $@) {
                warn "could not write history file: $@"
            }
        }
        $SIG{INT}= defined($oldsigint)? $oldsigint : "DEFAULT";
        # (Is there no other return path from sub run? should I use
        # DESTROY objects for this? -> nope, no returns, but if
        # exceptions not trapped it would fail)
    }

    # restore previous history, if any
    if ($current_history) {
        $term->clear_history;
        for (@$current_history) {
            chomp;
            $term->addhistory($_);
        }
    }
}


end Chj::Class::Array;

# for backwards compatibility:
*set_maxhistlen= *set_maxHistLen{CODE};
*set_docatchint= *set_doCatchINT{CODE};
*set_dorepeatwhenempty= *set_doRepeatWhenEmpty{CODE};
*set_maybe_keepresultin= *set_maybe_keepResultIn{CODE};

