Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Day 7: Some Assembly Required

2015/7.livemd

Day 7: Some Assembly Required

Mix.install([
  {:kino, "~> 0.12.3"}
])

Modules

defmodule Circuit do
  @moduledoc """
  Functions for assembling and inspecting circuits.
  """

  @u16_size 65536

  @typedoc "A circuit"
  @type t :: %{atom() => connection()}

  @typedoc "An instruction for building the circuit"
  @type instruction :: {atom(), connection()}

  @typedoc "The connections possible for each instruction"
  @type connection ::
          {:AND, connection(), connection()}
          | {:OR, connection(), connection()}
          | {:LSHIFT, connection(), {:SIGNAL, non_neg_integer()}}
          | {:RSHIFT, connection(), {:SIGNAL, non_neg_integer()}}
          | {:NOT, connection()}
          | {:WIRE, atom()}
          | {:SIGNAL, non_neg_integer()}

  @doc """
  Builds a circuit from a list of instructions.

  ## Example
  ```elixir
  iex> [{:fy, {:NOT, {:WIRE, :fx}}}, {:fx, {:SIGNAL, 10}}]
  iex> |> Circuit.build()
  %{
    fy: {:NOT, {:WIRE, :fx}}, 
    fx: {:SIGNAL, 10},
  }
  ```
  """
  @spec build(list(instruction())) :: t()
  def build(instructions) do
    instructions
    |> Enum.reduce(%{}, fn
      instruction, circuit ->
        {wire, connection} = instruction
        Map.put(circuit, wire, connection)
    end)
  end

  @doc """
  Calculates the signal on a given wire.

  ## Example
  ```elixir
  iex> %{a: {:LSHIFT, {:WIRE, :b}, {:SIGNAL, 1}}, b: {:SIGNAL, 8}}
  iex> |> Circuit.get_signal(:a)
  16
  ```
  """
  def get_signal(circuit, wire) do
    resolve(circuit, circuit[wire], %{})
    |> elem(0)
  end

  @doc """
  Overrides a signal for a specific wire identifier.

  ## Example
  ```elixir
  iex> %{b: {:SIGNAL, 10}}
  iex> |> Circuit.put_signal(:b, 50)
  %{b: {:SIGNAL, 50}}
  ```
  """
  def put_signal(circuit, wire, value) do
    Map.put(circuit, wire, {:SIGNAL, value})
  end

  # recursively resolves a connection, with the aid of a cache
  defp resolve(circuit, connection, cache)

  # when value is in the cache
  defp resolve(_circuit, {:WIRE, wire}, cache) when is_map_key(cache, wire),
    do: {cache[wire], cache}

  # when value is not in the cache
  defp resolve(circuit, {:WIRE, wire}, cache) do
    {result, cache} = resolve(circuit, circuit[wire], cache)
    {result, Map.put(cache, wire, result)}
  end

  defp resolve(_circuit, {:SIGNAL, value}, cache), do: {value, cache}

  defp resolve(circuit, {:NOT, a}, cache),
    do: unary(circuit, a, &Bitwise.bnot/1, cache)

  defp resolve(circuit, {:AND, a, b}, cache),
    do: binary(circuit, a, b, &Bitwise.&&&/2, cache)

  defp resolve(circuit, {:OR, a, b}, cache),
    do: binary(circuit, a, b, &Bitwise.|||/2, cache)

  defp resolve(circuit, {:LSHIFT, a, value}, cache),
    do: binary(circuit, a, value, &amp;Bitwise.<<>>/2, cache)

  # used to resolve a single operand and apply a unary operation
  # to the results
  # the final result is reduced to fit in 16 bits
  defp unary(circuit, right, function, cache) do
    with {right, cache} <- resolve(circuit, right, cache) do
      {function.(right) |> Integer.mod(@u16_size), cache}
    end
  end

  # used to resolve a pair of operands and apply a binary operation
  # to the results
  # the final result is reduced to fit in 16 bits
  defp binary(circuit, left, right, function, cache) do
    with {left, cache} <- resolve(circuit, left, cache),
         {right, cache} <- resolve(circuit, right, cache) do
      {function.(left, right) |> Integer.mod(@u16_size), cache}
    end
  end
end
defmodule Parser do
  @moduledoc """
  For parsing a string into circuit instructions.
  """

  @doc ~S"""
  Parses a string into a list of instructions.
  """
  def parse(str) do
    str
    |> String.split("\n")
    |> Enum.map(&amp;parse_line/1)
  end

  # parses single line into instruction form
  defp parse_line(str) do
    [expr | [dest]] = String.split(str, " -> ")
    wire = String.to_atom(dest)
    op = Regex.run(~r/AND|OR|LSHIFT|RSHIFT|NOT/, expr) |> parse_op()
    operands = Regex.scan(~r/\d+|[a-z]+/, expr) |> parse_operands()
    {wire, parse_connection(op, operands)}
  end

  # parses the connection
  defp parse_connection(op, operands)

  # for :SIGNAL and :WIRE connections
  defp parse_connection(nil, [operand]), do: operand

  # for unary connections :NOT
  defp parse_connection(op, [a]), do: {op, a}

  # for binary connections :AND, :OR, :LSHIFT and :RSHIFT
  defp parse_connection(op, [a, b]), do: {op, a, b}

  # creates atom of operation match results
  defp parse_op(op_matches)
  defp parse_op(nil), do: nil
  defp parse_op([op]), do: String.to_atom(op)

  # ensures we have a flat list of integers or identifiers as operands
  defp parse_operands(operand_matches) do
    operand_matches
    |> List.flatten()
    |> Enum.map(&amp;parse_operand/1)
  end

  # parses single operand
  defp parse_operand(operand) do
    Integer.parse(operand)
    |> case do
      :error -> {:WIRE, String.to_atom(operand)}
      {integer, _} -> {:SIGNAL, integer}
    end
  end
end

Input

input = Kino.Input.textarea("Please paste your puzzle input:")

Part 1

input
|> Kino.Input.read()
|> Parser.parse()
|> Circuit.build()
|> Circuit.get_signal(:a)

Part 2

circuit =
  Kino.Input.read(input)
  |> Parser.parse()
  |> Circuit.build()

a = Circuit.get_signal(circuit, :a)

circuit
|> Circuit.put_signal(:b, a)
|> Circuit.get_signal(:a)