Finding flow while coding is one of the joys of programming.
Encountering simple syntactic bugs, however, can sometimes interrupt flow. A single missing semicolon, for example, can result in a “WAT!?” followed by a “DOH!”
Perl 6 helps you around the code -> run -> fix cycle by identifying the cause and location of a bug and often suggesting a solution. Take this program:
say "hello" say "world";
When you run it, Perl 6 will suggest what’s wrong, where the problem is, and what to do next …
===SORRY!=== Error while compiling /home/nige/hello-world.pl Two terms in a row across lines (missing semicolon or comma?) at /home/nige/hello-world.pl:6 ------> say "hello"⏏
That helps to keep things flowing.
Normally, at this point, it’s off to your $EDITOR to manually add the semicolon and the cycle repeats.
What if Perl 6 could suggest a fix and apply it for you?
Introducing perl6fix
Here is the beginnings of a command-line utility called, perl6fix, that takes the hint from Perl 6 and tries to speed up the code -> run -> fix cycle by applying the fix for you.
Let’s look at the code.
It needs a handle on bug descriptions found in Perl 6 output.
class Bug { has Int $.line-number; has SourceFile $.source-file; has Str $.description; has Str $.pre-context; }
And a way to describe fixes:
class Fix { has $.prompt; has $.pattern; has $.action; method apply ($bug) { $!action($bug); } method applies-to ($bug) { return True without $.pattern; return $bug.description ~~ $.pattern; } }
And a way to update the source file:
class SourceFile is IO::Path { has @!content-lines = self.IO.slurp.lines; method append-to-first-matching-line ($append-char, $text-to-match) { return unless my $first-matching-index = @!content-lines.first(rx/$text-to-match/, :k); @!content-lines[$first-matching-index] ~= $append-char; self.save; } method swap-characters-on-line ($from-chars, $to-chars, $line-number) { @!content-lines[$line-number - 1].subst-mutate(/$from-chars/, $to-chars); self.save; } method save { self.IO.spurt(@!content-lines.join("\n")); } }
Here is just some of the fixes I encountered while writing this program:
my @fixes = ( Fix.new( prompt => 'add semicolon', pattern => rx/'missing semicolon or comma?'/, action => sub ($bug) { $bug.source-file.append-to-first-matching-line(';', $bug.pre-context); } ), Fix.new( prompt => 'add comma', pattern => rx/'missing semicolon or comma?'/, action => sub ($bug) { $bug.source-file.append-to-first-matching-line(',', $bug.pre-context); } ), Fix.new( prompt => 'convert . to ~', pattern => rx/'Unsupported use of . to concatenate strings; in Perl 6 please use ~'/, action => sub ($bug) { $bug.source-file.swap-characters-on-line('.', '~', $bug.line-number); } ), Fix.new( prompt => 'convert qr to rx', pattern => rx/'Unsupported use of qr for regex quoting; in Perl 6 please use rx'/, action => sub ($bug) { $bug.source-file.swap-characters-on-line('qr', 'rx', $bug.line-number); } ), # ADD YOUR OWN FIXES HERE );
There’s many more potential fixes (I’m just starting).
The perl6fix script wraps perl6, captures STDERR (if there is any), and then looks for a bug report in the output:
sub find-bug ($perl6-command) { return unless my $error-output = capture-stderr($perl6-command); # show the error note($error-output); # re-run the command again - this time grabbing a JSON version of the bug # set RAKUDO_EXCEPTIONS_HANDLER env var to JSON return unless my $error-as-json = capture-stderr('RAKUDO_EXCEPTIONS_HANDLER=JSON ' ~ $perl6-command); return unless my $bug-description = from-json($error-as-json); # just handle these exception types to start with for 'X::Syntax::Confused', 'X::Obsolete' -> $bug-type { next unless my $bug = $bug-description{$bug-type}; return Bug.new( description => $bug<message>, source-file => SourceFile.new($bug<filename>), line-number => $bug<line>, pre-context => $bug<pre> ); } }
The next step is to see if there are any fixes for this type of bug:
sub fix-bugs ($perl6-command-line) { my $bug = find-bug($perl6-command-line); unless $bug { say 'No bugs found to fix.'; exit; } # find a potential list of fixes for this type of bug my @found-fixes = @fixes.grep(*.applies-to($bug)); say $bug.description; say $bug.source-file.path ~ ' ' ~ $bug.line-number; say 'Suggested fixes found: '; my $fix-count = 0; for @found-fixes -> $fix { $fix-count++; my $option = ($fix-count == 1) ?? "*[1] " ~ $fix.prompt !! " [$fix-count] " ~ $fix.prompt; say ' ' ~ $option; } my $answer = prompt('apply fix [1]? ') || 1; my $fix = @found-fixes[$answer - 1]; $fix.apply($bug); # look for more bugs! until we're done fix-bugs($perl6-command-line); }
With the help of a shell alias you can even run it to fix the previous perl6 command. Like so:
Just add an alias to your bash or zsh profile. For example:
alias fix='/home/nige/perl6/perl6fix prev $(fc -ln -1)'
Now it’s your turn
As you can see this is just a start.
You’re welcome to take the full script and evolve your own set of automatic Perl 6 fixes.
You could even adapt the script to apply fixes for Perl 5 or other languages? What about a version using grammars? Or a macro-powered version integrated directly into Perl 6?
Well maybe that last one is something to look forward to next Christmas!
Hope you have a happy Christmas and a flowing, Perl 6 powered 2017!
Here’s something that may make your script easier to write and more robust: Rakudo actually supports custom exception handlers via RAKUDO_EXCEPTIONS_HANDLER env var (try setting it to `JSON` to get exceptions in JSON format).
So instead of parsing STDERR, you could set your own custom handler and introspect exceptions via their methods instead.
Great tip! That should make things a lot easier.
I’ve just updated the code to use JSON parsing instead: https://github.com/nige123/perl6fix/blob/master/perl6fix