aoc2019/lib/intcode.ex -rw-r--r-- 3.1 KiB View raw
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
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