Day 19 – Fixing Flow

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.

hello-world

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:

fix-image

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!

5 thoughts on “Day 19 – Fixing Flow

  1. 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.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.