Devel::REPL: now with PPI!

brian_d_foy on 2007-09-22T21:15:00

I've been using Devel::REPL for a while now. Like all good modules (Perl::Critic, POE, Plagger, etc), it's very extensible. Devel::REPL's design is worth studying: keep a simple core and ship all the fancy behavior as plugins. Moose amplifies the power and convenience of this design with roles, method modifiers, and general awesomeness.

There are plugins to dump output with Data::Dump::Streamer, enable tab completion of the current lexical environment and loaded modules, save input history across sessions, and more. If you dabble in other P-languages such as Python and Ruby (know thy enemy.. honest!) you'll find yourself wanting more out of Devel::REPL. Let's take the example of writing a factorial function in python:

% python
Python 2.3.5 (#1, Dec  7 2006, 14:50:51)
[GCC 4.0.1 (Apple Computer, Inc. build 5363) (+4864187)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> def fact(n):
...     if n < 2:
...         return 1
...     return n * fact(n - 1)
...
>>> fact(10)
3628800

in irb:

% irb
irb(main):001:0> def fact(n)
irb(main):002:1> if n < 2
irb(main):003:2> 1
irb(main):004:2> else
irb(main):005:2* n * fact(n-1)
irb(main):006:2> end
irb(main):007:1> end
=> nil
irb(main):008:0> fact 10
=> 3628800

and in Devel::REPL:

% re.pl
$ sub fact {
Compile error: Missing right curly or square bracket at (eval 60) line
8, at end of line
syntax error at (eval 60) line 8, at EOF
D'oh. OK, let's try again..

% re.pl
$ sub fact { my $n = shift; return 1 if $n < 2; $n * fact($n - 1) }

$ fact 10
3628800

Well, that works in this case, but one big line of code quickly becomes unmanageable.

Recently I had the idea to use PPI to figure out if the current line of code is complete. PPI::Dumper quickly confirmed that I can detect the most important case: a PPI::Structure that doesn't have both a ->start and ->finish. Structures encompass { {nested { blocks } } }, (parentheses), [array indexing], {hash indexing}, and so on. Hopefully future versions of PPI will be able to figure out that, say, an s/// or quoted string is incomplete.

Here's what the factorial example looks like with the MultiLine::PPI plugin.

% re.pl
$ load_plugin 'MultiLine::PPI'
1
$ sub fact {
> my $n = shift;
> return 1 if $n < 2;
> $n * fact($n - 1);
> }

$ fact 10
3628800

I believe Devel::REPL is the only Perl REPL that can do this. Hooray! :)

MultiLine::PPI hasn't been CPANed yet, but you can get it (and other new plugins) from the Devel::REPL Subversion repository.

I'll continue stealing good features from irb and python.


fantastic

rjbs on 2007-09-22T03:16:26

That's long overdue! Good work!

"Only"?

educated_foo on 2007-09-22T04:54:40

The only multi-line REPL? Not true! Sepia also does this, by trying to eval the input so far and parsing $@ to guess what went wrong. It's not perfect, but it does a decent job of distinguishing between incomplete input and internal syntax errors.

Re:"Only"?

sartak on 2007-09-22T07:17:35

Ah. I've seen Sepia before in search results, but I've never actually checked it out (seeing as how I'm a vim guy).

Interesting way to solve the same problem. Both have their strengths and weaknesses (PPI is more correct and won't run BEGIN/CHECK blocks, Sepia probably handles more cases, such as s/// and quoted strings)

Re:"Only"?

educated_foo on 2007-09-22T07:50:18

It *should* be possible to integrate Sepia's REPL with vim, or to use it separately with e.g. rlwrap, though I haven't tried it much. And the approach I take is mostly inferior to PPI, other than having far fewer prerequisites.

PPI handles the two cases you want

Alias on 2007-09-23T23:57:39

I'm almost certain there is enough information in place in the PPI tree to catch the two cases you want (incomplete regex and incomplete strings, plus you forgot incomplete heredocs, which PPI already detects in ->serialize).

Re:PPI handles the two cases you want

sartak on 2007-09-24T03:44:38

Great! I'll have a look soon. I suspected there wasn't because I didn't see anything like the '???' of incomplete structures in the output of PPI::Dumper.

Re:PPI handles the two cases you want

Alias on 2007-09-24T07:44:46

It's a little more complex and slightly different in each case, but as an example, look at the exact numbers in the Perl structure of an incomplete quote, you can probably intuit it.

What might be a better idea though, is to introduce an ->complete or ->incomplete method at the PPI::Element level and abstract the entire thing behind a nice interface.

Re:PPI handles the two cases you want

sartak on 2007-09-25T02:20:54

What might be a better idea though, is to introduce an ->complete or ->incomplete method at the PPI::Element level and abstract the entire thing behind a nice interface.

That would be great. I've taken a stab at implementing it and it looks to be way over my head.