Andrew Fontaine

Mostly code

Advent of Code 2019: Day 5

06 Dec 2019
Code Snippet for Advent of Code 2019: Day 5

In which previous bad code shoots me in the foot.

This was a continuation of the puzzle from day 2. If you don’t recall, I made a small intcode computer that was able to add, multiply, and stop. I need to augment this computer by adding the ability to take input and produce output. I also need to handle “parameter modes” and expanded intcodes

First, I want to be able to handle expanded intcodes. I also wanted to ensure it is backwards compatible with day 2. intcodes are now 5 digit numbers, where the first three digits indicate the parameter mode for a given parameter and the final 2 digits are the operational code.

  def run(%{program: prog, position: pos} = computer) do
    code =
      prog
      |> Enum.at(pos)
      |> Integer.digits()
      |> pad_code()

    op(computer, code)
  end

  defp pad_code(code) do
    List.duplicate(0, 5 - length(code)) ++ code
  end

As leading zeros are not included when calling Integer.digits/2, I just pad them at the front once I have a list so [1, 0, 2] becomes [0, 0, 1, 0, 2]. This also makes the codes from day 2 into “expanded” codes, and things will continue to work. To handle parameter modes, I will create a helper function that checks the mode and returns the correct value for the parameter.

  defp parameter(0, x, prog), do: Enum.at(prog, x)

  defp parameter(1, x, _prog), do: x

  defp parameter(_, x, prog), do: parameter(0, x, prog)

A mode of 0 indicates the parameter is a pointer to somewhere else in the program, which is exactly how day 2 operated. A mode of 1 indicates that the value is the parameter.

  • Mode 0: A parameter of 3 means the value is found at index 3 of the program list, so I must find and return it.
  • Mode 1: A parameter of 3 means the value is 3, so I can return it immediately.

I added a default case that ensures the mode is 0 in case something goes wrong, but crashing here would probably be better. As these parameter codes also affected the output of an opcode, I created a helper to handling storing values (it would come in handy when handling input as well).

  defp store(prog, pos, v, m) do
    List.update_at(prog, parameter(m, pos, prog), fn _ -> v end)
  end

While I’m sure output would always be mode 0, it doesn’t hurt to handle it just in case there’s a throwaway value. The problem also doesn’t explicitly state one way or the other, so I’d rather be safe than sorry.

I’ll also make a small helper for shifting the position of the program, to cover all my bases.

  defp shift(pos, x), do: pos + x

Again, probably unnecessary but it felt right.

Now that I have a bunch of helpers, I’ll amplify my existing op/2 functions to handle the new modes with a new compute/3 method.

  def op(computer, [a, b, c, 0, 1]),
    do: {:ok, compute(computer, &Kernel.+/2, [a, c, b])}

  def op(computer, [a, b, c, 0, 2]),
    do: {:ok, compute(computer, &Kernel.*/2, [a, c, b])}

  defp compute(%__MODULE__{program: prog, position: pos} = computer, f, [m | modes] = args) do
    vals =
      modes
      |> Enum.with_index(1)
      |> Enum.map(fn {m, i} -> parameter(m, Enum.at(prog, pos + i), prog) end)

    prog = store(prog, pos + length(args), run(f, vals), m)

    pos = shift(pos, length(args) + 1)

    %__MODULE__{computer | program: prog, position: pos}
  end

  defp run(f, args) do
    apply(f, args)
  end

OK, so compute/3 is the big many-step process that should run the things:

  1. Fetch the values for the parameters for the op-code
  2. Run the provided elixir function for the op-code with the given parameters
  3. Store the result in the appropriate location
  4. Shift the program pointer the right number of steps
  5. Return an updated %Intcode{} struct.

This is still compatible with the problem from day 2! Time for the new instructions.

Input and Output

I don’t like the idea of actually prompting for input and printing output, so I added some lists to keep track of it.

  defstruct program: [],
            position: 0,
            input: [],
            output: []

and alter new to allow the passing in of input

  def new(program, input \\ []), do: %__MODULE__{program: program, input: input}

At this point, I realized I’d have to pass input and output all around, and decided (after an hour of mulling it over), to just make some new methods for them specifically

  def op(computer, [_a, _b, c, 0, 3]),
    do: {:ok, input(computer, c)}

  def op(computer, [_a, _b, c, 0, 4]),
    do: {:ok, output(computer, c)}

  defp output(%__MODULE__{program: prog, position: pos, output: out} = computer, m) do
    output = out ++ [parameter(m, Enum.at(prog, pos + 1), prog)]

    pos = shift(pos, 2)

    %__MODULE__{computer | program: prog, position: pos, output: output}
  end

  defp input(%__MODULE__{program: prog, position: pos, input: [i | t]}, m) do
    prog = store(prog, pos + 1, i, m)
    %__MODULE__{program: prog, position: pos + 2, input: t}
  end

All that’s left is to update run_all to ensure I can access the output and do the puzzle!

  def run_all({:halt, computer}), do: computer
import AdventOfCode
alias AdventOfCode.Y2019.Utils.Intcode

aoc 2019, 5 do
  def p1(), do: compute([1])

  def compute(i) do
    input_stream()
    |> Enum.at(0)
    |> String.split(",")
    |> Enum.map(&String.to_integer/1)
    |> Intcode.new(i)
    |> Intcode.run_all()
    |> Map.get(:output)
  end
end

⭐ one done!

Puzzle 2

Oh wow more instructions! Let’s get hopping!

Jumps

To handle the jumps, I added a jump/4 function that checks if the provided function is true and jumps!

  def op(computer, [_a, b, c, 0, 5]),
    do: {:ok, jump(computer, c, b, &Kernel.!=/2)}

  def op(computer, [_a, b, c, 0, 6]),
    do: {:ok, jump(computer, c, b, &Kernel.==/2)}

  defp jump(%__MODULE__{program: prog, position: pos} = computer, a, b, f) do
    if f.(0, parameter(a, Enum.at(prog, pos + 1), prog)) do
      %__MODULE__{computer | position: parameter(b, Enum.at(prog, pos + 2), prog)}
    else
      %__MODULE__{computer | position: pos + 3}
    end
  end

Comparisons

Comparisons, thankfully, work exactly as the first two op-codes, so adding them is as simple as the following.

  def op(computer, [a, b, c, 0, 7]),
    do: {:ok, compute(computer, &Kernel.</2, [a, c, b])}

  def op(computer, [a, b, c, 0, 8]),
    do: {:ok, compute(computer, &Kernel.==/2, [a, c, b])}

After some quick testing, and more writing, puzzle 2 is done.

  def p2(), do: compute([5])

Conclusion

There were a number of hiccups along the way. Again, I started way too late. I also spent a very long time trying to make compute/3 handle all of my op-codes. I still think its possible, but I was running out of time and needed to get something done. I also misread a test case, so my first answer of Puzzle 2 was wrong, again staying up too late. The worst part is that my code worked correctly when I wrote it, and the bad test case made me change it. In the end, I was successful.

🌟 abound, and ready for the next day!

I ❤ feedback. Let me know what you think of this article on Twitter @afontaine_ca