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 intcode
s
First, I want to be able to handle expanded intcode
s. I also wanted to ensure
it is backwards compatible with day 2. intcode
s 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 of3
means the value is found at index3
of the program list, so I must find and return it. - Mode
1
: A parameter of3
means the value is3
, 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:
- Fetch the values for the parameters for the op-code
- Run the provided elixir function for the op-code with the given parameters
- Store the result in the appropriate location
- Shift the program pointer the right number of steps
- 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!