Advent of Code, Intcode & subroutine signatures

As you hopefully are aware of, Advent of Code has started again on the 1st December. On day 2 you have to write an interpreter for something called Intcode (I won't repeat the specs here, so you might want to read the details on adventofcode.com). After solving the first part you get this hint:

"Keep [your code] nearby during this mission - you'll probably use it again. Real Intcode computers support many more features than your new one, but we'll let you know what they are as you need them."

So I packed my ugly-as-hell hacked together code I wrote in the morning and put it into a proper Perl module. To make things at least a bit interesting, I decided to use the new (still experimental) Signatures feature and some fancy postfix deref array slices (near the end of this post..)

Without further ado, the code

Intcode.pm

package Intcode;

use strict;
use warnings;
use 5.030;
use feature 'signatures';
no warnings 'experimental::signatures';

use base qw(Class::Accessor::Fast);
__PACKAGE__->mk_accessors(qw(pos code));

After the obvious package declaration and using strict & warnings, I enable the signature feature and disable the experimental::signature warnings. I do hope that this boilerplate will be not necessary soon (maybe in 5.32?).

Instead of using a full object system like Moose, I just grab the old & battle-tested Class::Accessor::Fast and define two accessors, pos (the current position in the list of codes) and code (the actual input code).

sub new ($class, $code, $pos = 0) {
    return bless {
        code => $code,
        pos  => $pos,
    }, $class;
}

I don't use the default constructor provided by Class::Accessor, because I want to simplify constructing a new object using the following API:

my $ic = Intcode->new( [ 1,0,0,3,99 ] );

So I define three positional arguments: $class, $code and $pos. As $pos is defined with a default ($pos = 0), it is interpreted as an optional argument, which makes for an even cleaner initialization (at least for now, let's see what future tasks will bring..)

Using $code and $pos, I create a Hashref, which I bless into the $class (using very old-school Perl). The such created object is then returned to the caller.

Using Intcode

Here's the solution to the first part, using Intcode:

use 5.030;
use strict;
use warnings;
use lib '.';
use Intcode;

my @code = split( ',', <STDIN> );
$code[1] = 12;
$code[2] = 2;

my $intcode = Intcode->new( [@code] );
say $intcode->runit;

After the boilerplate, I get the input from STDIN and split it into an array. Then set (as per the instructions) the two first input values and generate a new Intcode instance using the parsed code. Now I only have to call runit to get the result.

runit

sub runit ($self, $final = 0) {
    while (1) {
        my $op     = $self->code->[ $self->pos ];
        my $method = 'op_' . $op;
        last unless defined $self->$method;
    }
    return $self->code->[$final];
}

runit again uses signatures instead of manual @_ unpacking, though I wish there was method keyword that would automatically set up $self. $final is another optional parameter defaulting to 0. It might be used in the future to find the position of the element to report back; on day 2 this always was the first element (i.e. 0 in computer count), so I consider this some future-proofing...

Inside the method I run an endless while loop. I fetch the op code from the current position ($self->pos), which in the first iteration is 0. I generate a method name base on the value I find at the position, eg op_1 or op_2. Then I call this method. If it returns undef (from op_99) I exit the loop and return the value at the $final position. If it returns something else, the loop continues.

@h4. op_99

sub op_99 {
    return undef;
}

Now that's some boring code...

op_1 and op_2

sub op_1 ($self) {
    my ( $x, $y, $t ) = $self->get_n( 3 );
    $self->code->[$t] = $self->code->[$x] + $self->code->[$y];
}

sub op_2 ($self) {
    my ( $x, $y, $t ) = $self->get_n( 3 );
    $self->code->[$t] = $self->code->[$x] * $self->code->[$y];
}

Basically the same, the only difference is the mathematical operator + vs *. I use a helper method get_n (more on that in a minute) to get the needed amount of codes (in this case, 3: the location of the two summands and the location of the target), and the assign the target ($t) the result of the calculation (addition or multiplication) using the values stored at the respective locations. As Perl automatically returns the value of the last expression, the while loop in runit will continue to run.

get_n

sub get_n ($self, $n) {
    my $pos = $self->pos;
    my @pointer = $self->code->@[ $pos + 1 .. $pos + $n ];
    $self->pos( $pos + $n + 1 );
    return @pointer;
}

This method contains the only (at least slightly...) smart code. It gets the object $self and a count $n, again via subroutine signatures. The we get the current position $pos. We now use another newish Perl feature postfix dereference combined with an array slice. Let's break this down a bit:

my @pointer = $self->code->@[ $pos + 1 .. $pos + $n ];
[ $pos + 1 .. $pos + $n ]

This defined a range starting at $pos + 1 going to $pos + n. I set $n = 3, so in the first loop (when $pos is 0) we get: 0+1 .. 0+3, i.e. 1, 2, 3.

$self->code->@[ 1, 2, 3];

arrayref->@[ list ] defines a slice from the the arrayref, i.e. we get back the list of values passed via the slice list.

So if $self->code is 1,0,0,3,99, then $self->code->@[1,2,3] will return (0,0,3).

I assign this list to @pointer.

But before returning @pointer, we have to move the position forward the correct amount of steps. On day 2, we always have to skip 3 steps, but I assume that later op codes will have a different amount of arguments. So instead of hardcoding this, we move forward the number of elements we needed to get, plus one (because we want to get to the element after the op args.

And that's it!

Here is my solution for the second task, again using Intcode:

use 5.010;
use strict;
use warnings;
use lib '.';
use Intcode;

my @code = split( ',', <STDIN> );

for my $n ( 0 .. 99 ) {
    for my $v ( 0 .. 99 ) {
        $code[1] = $n;
        $code[2] = $v;
        my $intcode = Intcode->new( [@code] );

        my $res = $intcode->runit;
        if ( $res == 19690720 ) {
            say "$n, $v -> " . ($n* 100 + $v);
            exit;
        }
    }
}

Oh, and please give Advent Of Code a try - it' really fun!

P.S.: If you want to join our (Vienna (and Prague( Perl (and other language) hackers) leaderboard, contact me for the key!