Compare commits

...

2 Commits

7 changed files with 453 additions and 111 deletions

View File

@ -98,7 +98,7 @@ defmodule Day5 do
# IO.inspect(memory)
case eval(Enum.drop(memory, ip), memory, parent) do
{memory, :halt} ->
{_memory, :halt} ->
send(parent, {:halt, self()})
{memory, :cont, offset} ->
@ -109,7 +109,7 @@ defmodule Day5 do
end
end
def run(memory, parent, _) do
def run(_memory, parent, _) do
send(parent, {:ok, self()})
end

View File

@ -1,155 +1,227 @@
defmodule Assembler do
import NimbleParsec
label =
ascii_string([?a..?z, ?A..?Z, ?_], min: 1)
|> ignore(ascii_char([?:]))
|> ignore(repeat(ascii_char([?\s])))
data_label =
label
|> choice([
integer(min: 1),
ignore(ascii_char([?-]))
|> integer(min: 1)
|> tag(:neg)
])
|> tag(:data_label)
param =
optional(ignore(ascii_string([?\s], min: 1)))
|> ascii_string([{:not, ?,}], min: 1)
instruction =
ignore(ascii_string([?\s], 2))
|> ascii_string([?a..?z], min: 1)
|> repeat(param |> ignore(ascii_char([?,])))
|> optional(param)
|> tag(:insn)
defparsec(
:parse_line,
choice([
data_label,
label |> tag(:label),
instruction,
eos()
])
)
@asm """
in #30
clt 30 #8 #31
jnz 31 #lessThan
ceq 30 #8 #31
jnz 31 #equal
out #1001
in res
clt $res, _self + 1, cmpRes
jnz $cmpRes, lessThan
ceq $res, 8, cmpRes
jnz $cmpRes, equal
out 1001
hlt
lessThan:
out #999
out 999
hlt
equal:
out #1000
out 1000
hlt
res: 0
cmpRes: 0
"""
def test do
@asm
|> IO.inspect()
|> assemble(32)
assemble(@asm)
end
def assemble(asm, pad_length \\ 0) do
def assemble(asm) do
{memory, labels} =
asm
|> String.trim_trailing("\n")
|> String.split("\n")
|> Enum.reject(&(String.length(&1) == 0))
|> Enum.reduce({[], %{}}, fn line, {memory, labels} ->
cond do
Regex.match?(~r/^\s+$/, line) ->
{memory, labels}
|> Enum.map(fn line ->
case parse_line(line) do
{:ok, res, _, _, _, _} ->
res
String.starts_with?(line, " ") ->
{
assemble_instruction(String.slice(line, 2..-1), memory),
labels
}
String.ends_with?(line, ":") ->
{
memory,
Map.put(labels, String.slice(line, 0..-2), length(memory))
}
true ->
IO.inspect("Ignoring line: #{line}")
err ->
raise "Unable to parse line: '#{line}':\n#{inspect(err)}"
end
end)
|> Enum.reduce({[], %{}}, &assemble_line/2)
memory =
Enum.map(memory, fn
{:label, name} -> Map.fetch!(labels, name)
it -> it
end)
memory
|> Enum.reverse()
|> Enum.with_index()
|> Enum.map(fn
{{:expr, expr}, index} ->
ExpressionEvaluator.eval(expr, fn
"_self" -> index
name -> Map.fetch!(labels, name)
end)
memory =
if pad_length > length(memory) do
repeat(0, pad_length - length(memory)) ++ memory
else
memory
end
IO.inspect(labels)
Enum.reverse(memory)
{it, _} ->
it
end)
end
def repeat(_, 0), do: []
def repeat(n, count), do: [n | repeat(n, count - 1)]
def assemble_line([], acc), do: acc
def assemble_line([label: [name]], {memory, labels}) do
{
memory,
Map.put(labels, name, length(memory))
}
end
def assemble_line([data_label: [name, value]], {memory, labels}) do
{
[value | memory],
Map.put(labels, name, length(memory))
}
end
def assemble_line([insn: insn], {memory, labels}) do
{
assemble_insn(insn, memory),
labels
}
end
def assemble_insn(["add" | params], memory) when length(params) == 3 do
{[a, b, dest], modes} = parse_params(params, [:read, :read, :write])
[dest, b, a, 1 + modes | memory]
end
def assemble_insn(["mul" | params], memory) when length(params) == 3 do
{[a, b, dest], modes} = parse_params(params, [:read, :read, :write])
[dest, b, a, 2 + modes | memory]
end
def assemble_insn(["in", dest], memory) do
{[dest], modes} = parse_params([dest], [:write])
[dest, 3 + modes | memory]
end
def assemble_insn(["out", src], memory) do
{[src], modes} = parse_params([src], [:read])
[src, 4 + modes | memory]
end
def assemble_insn(["jnz" | params], memory) when length(params) == 2 do
{[target, dest], modes} = parse_params(params, [:read, :read])
[dest, target, 5 + modes | memory]
end
def assemble_insn(["jez" | params], memory) when length(params) == 2 do
{[target, dest], modes} = parse_params(params, [:read, :read])
[dest, target, 6 + modes | memory]
end
def assemble_insn(["clt" | params], memory) when length(params) == 3 do
{[a, b, dest], modes} = parse_params(params, [:read, :read, :write])
[dest, b, a, 7 + modes | memory]
end
def assemble_insn(["ceq" | params], memory) when length(params) == 3 do
{[a, b, dest], modes} = parse_params(params, [:read, :read, :write])
[dest, b, a, 8 + modes | memory]
end
def assemble_insn(["hlt"], memory) do
[99 | memory]
end
@doc """
Raises the base to to the given power.
## Examples
iex> Assembler.pow(10, 3)
1000
iex> Assembler.pow(5, 4)
625
"""
def pow(base, 1), do: base
def pow(base, exp), do: base * pow(base, exp - 1)
@doc """
Parses assembly instruction parameter values and modes.
## Examples
iex> Assembler.parse_params(["1", "2", "3"], [:read, :read, :read])
{[1, 2, 3], 11100}
iex> Assembler.parse_params(["1", "2", "3"], [:read, :read, :write])
{[1, 2, 3], 1100}
iex> Assembler.parse_params(["1", "2", "$3"], [:read, :read, :write])
{[1, 2, 3], 1100}
iex> Assembler.parse_params(["$1", "2", "3"], [:read, :read, :write])
{[1, 2, 3], 1000}
iex> Assembler.parse_params(["$label", "2", "3"], [:read, :read, :write])
{[{:expr, "label"}, 2, 3], 1000}
iex> Assembler.parse_params(["1", "label", "3"], [:read, :read, :write])
{[1, {:expr, "label"}, 3], 1100}
"""
def parse_params(params, param_types) do
params
|> String.split(" ")
|> Enum.zip(param_types)
|> Enum.with_index()
|> Enum.map_reduce(0, fn {{param, type}, index}, modes ->
val =
case Regex.run(~r/^#?(\d+)$/, param) do
case Regex.run(~r/^\$?(\d+)$/, param) do
[_, digits] ->
String.to_integer(digits)
_ ->
[_, name] = Regex.run(~r/^#(\w+)$/, param)
{:label, name}
case param do
"$" <> param ->
{:expr, param}
_ ->
{:expr, param}
end
end
modes =
if type == :read && String.starts_with?(param, "#") do
modes + pow(10, index + 2)
else
modes
case type do
:read ->
if String.starts_with?(param, "$") do
modes
else
modes + pow(10, index + 2)
end
_ ->
modes
end
{val, modes}
end)
end
def assemble_instruction(insn, memory \\ [])
def assemble_instruction("add " <> params, memory) do
{[a, b, dest], modes} = parse_params(params, [:read, :read, :write])
[dest, b, a, 1 + modes | memory]
end
def assemble_instruction("mul " <> params, memory) do
{[a, b, dest], modes} = parse_params(params, [:read, :read, :write])
[dest, b, a, 2 + modes | memory]
end
def assemble_instruction("hlt", memory) do
[99 | memory]
end
def assemble_instruction("in " <> params, memory) do
{[dest], modes} = parse_params(params, [:write])
[dest, 3 + modes | memory]
end
def assemble_instruction("out " <> params, memory) do
{[val], modes} = parse_params(params, [:read])
[val, 4 + modes | memory]
end
def assemble_instruction("jnz " <> params, memory) do
{[target, dest], modes} = parse_params(params, [:read, :read])
[dest, target, 5 + modes | memory]
end
def assemble_instruction("jez " <> params, memory) do
{[target, dest], modes} = parse_params(params, [:read, :read])
[dest, target, 6 + modes | memory]
end
def assemble_instruction("clt " <> params, memory) do
{[a, b, dest], modes} = parse_params(params, [:read, :read, :write])
[dest, b, a, 7 + modes | memory]
end
def assemble_instruction("ceq " <> params, memory) do
{[a, b, dest], modes} = parse_params(params, [:read, :read, :write])
[dest, b, a, 8 + modes | memory]
end
end

View File

@ -0,0 +1,106 @@
defmodule ExpressionEvaluator do
import NimbleParsec
number = integer(min: 1)
variable = ascii_string([?a..?z, ?A..?Z, ?_], min: 1)
whitespace = ascii_string([?\s], min: 1)
factor =
choice([
ignore(ascii_char([?(]))
|> concat(parsec(:expr))
|> ignore(ascii_char([?)])),
number,
variable,
ignore(ascii_char([?-]))
|> concat(number)
|> tag(:neg),
ignore(ascii_char([?-]))
|> concat(variable)
|> tag(:neg)
])
defcombinatorp(
:term,
choice([
factor
|> optional(ignore(whitespace))
|> ignore(ascii_char([?*]))
|> optional(ignore(whitespace))
|> concat(parsec(:term))
|> tag(:mul),
factor
|> optional(ignore(whitespace))
|> ignore(ascii_char([?/]))
|> optional(ignore(whitespace))
|> concat(parsec(:term))
|> tag(:div),
factor
])
)
defcombinatorp(
:expr,
choice([
parsec(:term)
|> optional(ignore(whitespace))
|> ignore(ascii_char([?+]))
|> optional(ignore(whitespace))
|> concat(parsec(:expr))
|> tag(:add),
parsec(:term)
|> optional(ignore(whitespace))
|> ignore(ascii_char([?-]))
|> optional(ignore(whitespace))
|> concat(parsec(:expr))
|> tag(:sub),
parsec(:term)
])
)
defparsec(:parse, parsec(:expr))
def eval(expr, vars \\ %{})
def eval(expr, vars) when is_map(vars) do
eval(expr, fn name ->
Map.fetch!(vars, name)
end)
end
def eval(expr, get_var) when is_function(get_var) do
{:ok, expr, _, _, _, _} = parse(expr)
do_eval(expr, get_var)
end
def do_eval([{op, [a, b]}], get_var) do
do_op(op, do_eval_single(a, get_var), do_eval_single(b, get_var))
end
def do_eval([it], get_var) do
do_eval_single(it, get_var)
end
def do_eval_single({:neg, [it]}, get_var) do
-1 * do_eval_single(it, get_var)
end
def do_eval_single(it, _) when is_integer(it) do
it
end
def do_eval_single(it, get_var) when is_binary(it) do
get_var.(it)
end
def do_eval_single(it, get_var) do
do_eval([it], get_var)
end
def do_op(:add, a, b), do: a + b
def do_op(:sub, a, b), do: a - b
def do_op(:mul, a, b), do: a * b
def do_op(:div, a, b), do: floor(a / b)
end

View File

@ -21,6 +21,7 @@ defmodule Aoc19.MixProject do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:nimble_parsec, "~> 0.5.2"}
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]

3
mix.lock Normal file
View File

@ -0,0 +1,3 @@
%{
"nimble_parsec": {:hex, :nimble_parsec, "0.5.2", "1d71150d5293d703a9c38d4329da57d3935faed2031d64bc19e77b654ef2d177", [:mix], [], "hexpm"},
}

View File

@ -0,0 +1,112 @@
defmodule AssemblerTest do
use ExUnit.Case
doctest Assembler
import Assembler
test "assembles empty program" do
assert assemble("") == []
end
test "assembles instructions correctly" do
assert assemble(" add 1, 2, 3") == [1101, 1, 2, 3]
assert assemble(" mul 1, 2, 3") == [1102, 1, 2, 3]
assert assemble(" in 7") == [3, 7]
assert assemble(" out 7") == [104, 7]
assert assemble(" out $7") == [4, 7]
assert assemble(" jnz 1, 2") == [1105, 1, 2]
assert assemble(" jnz $1, 2") == [1005, 1, 2]
assert assemble(" jez 1, 2") == [1106, 1, 2]
assert assemble(" jez $1, 2") == [1006, 1, 2]
assert assemble(" clt 1, 2, 3") == [1107, 1, 2, 3]
assert assemble(" clt $1, $2, $3") == [7, 1, 2, 3]
assert assemble(" ceq 1, 2, 3") == [1108, 1, 2, 3]
assert assemble(" ceq $1, $2, $3") == [8, 1, 2, 3]
assert assemble(" hlt") == [99]
end
test "assembles simple program" do
program = """
add 1, 2, 10
mul 2, 3, 11
add $10, $11, 12
hlt
"""
assert assemble(program) == [
1101,
1,
2,
10,
1102,
2,
3,
11,
1,
10,
11,
12,
99
]
end
test "assembles simple program with a label" do
program = """
add 1, 2, 10
jnz $10, nonZero
nonZero:
hlt
"""
assert assemble(program) == [
1101,
1,
2,
10,
1005,
10,
7,
99
]
end
test "assembles a simple program with a data label" do
program = """
add 1, 2, var
out $var
var: 0
"""
assert assemble(program) == [
1101,
1,
2,
6,
4,
6,
0
]
end
test "assembles a program with a _self label" do
program = """
add 1, 2, _self
"""
assert assemble(program) == [1101, 1, 2, 3]
end
test "assembles parameters with expressions" do
assert assemble(" add 1, _self + 1, 3") == [1101, 1, 3, 3]
assert assemble(" add 1, $(_self + 1), 3") == [101, 1, 3, 3]
assert assemble("""
add 1, label + 4 - 2, 3
label: 42
""") == [1101, 1, 6, 3, 42]
assert assemble("""
add 1, $(label + 4 - 2), 3
label: 42
""") == [101, 1, 6, 3, 42]
end
end

View File

@ -0,0 +1,48 @@
defmodule ExpressionEvaluatorTest do
use ExUnit.Case
doctest ExpressionEvaluator
import ExpressionEvaluator
test "evaluates individual numbers" do
assert eval("1") == 1
end
test "evaluates negated numbers" do
assert eval("-1") == -1
end
test "evaluates variables" do
assert eval("x", %{"x" => 3}) == 3
end
test "evaluates negated variables" do
assert eval("-x", %{"x" => 3}) == -3
assert eval("-x", %{"x" => -3}) == 3
end
test "evaluates binary operations" do
assert eval("1 + 2") == 3
assert eval("3 - 2") == 1
assert eval("3 * 2") == 6
assert eval("10 / 2") == 5
end
test "floors division" do
assert eval("3 / 2") == 1
end
test "obeys operator precedence" do
assert eval("1 + 2 * 3") == 7
assert eval("(1 + 2) * 3") == 9
assert eval("12 - 6 / 2") == 9
assert eval("(12 - 6) / 2") == 3
end
test "evaluates complex expressions" do
assert eval("1 + 2 * (3 - ((4 / 2 + 5) * 6))") == -77
end
test "evaluates complex expressions with variables" do
assert eval("x * ((4 - y) / 6 + z))", %{"x" => 2, "y" => -8, "z" => 5}) == 14
end
end