defmodule IntCode do
@moduledoc """
Models an IntCode computer as described in Day 2.
"""
defstruct [:code, ip: 0]
@type code :: [integer]
@type machine :: %IntCode{ip: integer, code: %{required(integer) => integer}}
@doc """
Creates a new IntCode machine from code given as a list of integers.
"""
@spec new(code) :: machine
def new(code) do
code =
code
|> Enum.with_index()
|> Enum.reduce(%{}, fn {value, line}, acc -> Map.put(acc, line, value) end)
%IntCode{code: code, ip: 0}
end
@doc """
Creates a new IntCode machine from code loaded from the given path.
"""
@spec load(String.t()) :: machine
def load(path) do
path
|> File.read!()
|> String.trim()
|> String.split(",")
|> Enum.map(&String.to_integer/1)
|> new()
end
@doc """
Returns the value stored at given position in the code.
"""
@spec get(machine, integer) :: integer
def get(%IntCode{code: code} = _machine, pos), do: code[pos]
@doc """
Sets the given position in code to the given value.
"""
@spec put(machine, integer, integer) :: machine
def put(%IntCode{code: code} = machine, pos, value) do
%IntCode{machine | code: Map.put(code, pos, value)}
end
@spec binary_op(machine, function) :: machine
defp binary_op(%IntCode{code: code, ip: ip} = machine, fun) do
val1 = code[code[ip + 1]]
val2 = code[code[ip + 2]]
code = Map.put(code, code[ip + 3], fun.(val1, val2))
%IntCode{machine | code: code, ip: ip + 4}
end
defp add(machine), do: binary_op(machine, &Kernel.+/2)
defp mul(machine), do: binary_op(machine, &Kernel.*/2)
@doc """
Performs a single step by executing the instruction at current instruction
pointer and returns the new machine state.
"""
@spec step(machine) :: machine
def step(%IntCode{code: code, ip: ip} = machine) do
case code[ip] do
1 -> add(machine)
2 -> mul(machine)
99 -> nil
end
end
@doc """
Returns a stream of machine states produced by repeatedly calling `step` from
the starting state until machine halts.
"""
@spec stream(machine) :: Enumerable.t()
def stream(machine) do
machine
|> Stream.iterate(&step/1)
|> Stream.take_while(&(not is_nil(&1)))
end
@doc """
Executes the code and returns the final machine state after it halts.
"""
@spec run(machine) :: machine
def run(machine) do
machine
|> stream()
|> Enum.at(-1)
end
end
defmodule IntCodeDebug do
def dump(machine) do
{machine.code, 0}
|> Stream.iterate(&dump_instruction/1)
|> Enum.take_while(fn {code, ip} -> Map.has_key?(code, ip) end)
|> Enum.count()
end
def dump_instruction({code, ip}) do
IO.write(String.pad_leading(to_string(ip), 3))
IO.write(" ")
case code[ip] do
1 ->
IO.puts("add #{code[ip + 1]} #{code[ip + 2]} -> #{code[ip + 3]}")
{code, ip + 4}
2 ->
IO.puts("mul #{code[ip + 1]} #{code[ip + 2]} -> #{code[ip + 3]}")
{code, ip + 4}
99 ->
IO.puts("HALT")
{code, ip + 1}
_ ->
IO.puts("??? #{code[ip]}")
{code, ip + 1}
end
end
end