From 49b2a79d0a6e3aaf22c9dfbe3920d647cdecb82c Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 7 Dec 2019 14:26:41 -0500 Subject: [PATCH] Add Nimble Parsed based arithmetic expression evaluator --- lib/intcode/assembler.ex | 199 +++++++++++++++++++++++----- lib/intcode/expression_evaluator.ex | 98 ++++++++++++++ mix.exs | 1 + mix.lock | 3 + test/intcode/assembler_test.exs | 79 +++++++++++ test/intcode/expr_test.exs | 48 +++++++ 6 files changed, 395 insertions(+), 33 deletions(-) create mode 100644 lib/intcode/expression_evaluator.ex create mode 100644 mix.lock create mode 100644 test/intcode/assembler_test.exs create mode 100644 test/intcode/expr_test.exs diff --git a/lib/intcode/assembler.ex b/lib/intcode/assembler.ex index b01e3cc..af13bfb 100644 --- a/lib/intcode/assembler.ex +++ b/lib/intcode/assembler.ex @@ -1,28 +1,31 @@ defmodule Assembler do @asm """ - in #30 - clt 30 #8 #31 - jnz 31 #lessThan - ceq 30 #8 #31 - jnz 31 #equal - out #1001 + in res + clt $res, 8 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() end - def assemble(asm, pad_length \\ 0) do + def assemble(asm) do {memory, labels} = asm |> String.split("\n") @@ -44,109 +47,239 @@ defmodule Assembler do Map.put(labels, String.slice(line, 0..-2), length(memory)) } + Regex.match?(~r/^\w+: \d+$/, line) -> + [name, value] = String.split(line, ": ") + value = String.to_integer(value) + + { + [value | memory], + Map.put(labels, name, length(memory)) + } + true -> IO.inspect("Ignoring line: #{line}") end end) - memory = - Enum.map(memory, fn - {:label, name} -> Map.fetch!(labels, name) - it -> it - end) + # IO.inspect(labels) - memory = - if pad_length > length(memory) do - repeat(0, pad_length - length(memory)) ++ memory - else - memory - end - - IO.inspect(labels) - Enum.reverse(memory) + memory + |> Enum.reverse() + |> Enum.with_index() + |> Enum.map(fn + {{:label, name}, index} -> get_label(name, index, labels) + {it, _} -> it + end) end - def repeat(_, 0), do: [] - def repeat(n, count), do: [n | repeat(n, count - 1)] + @doc """ + Get the address of a label. + `_self` is a special label that resolves to the address of where it's being inserted into the program. + + ## Examples + iex> Assembler.get_label("test", 1, %{"test" => 14}) + 14 + iex> Assembler.get_label("_self", 1, %{}) + 1 + """ + def get_label(name, index, labels) + + def get_label("_self", index, _) do + index + end + + def get_label(name, _, labels) do + Map.fetch!(labels, name) + 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]) + {[{:label, "label"}, 2, 3], 1000} + iex> Assembler.parse_params("1, label, 3", [:read, :read, :write]) + {[1, {:label, "label"}, 3], 1100} + """ def parse_params(params, param_types) do params - |> String.split(" ") + |> String.split(",") + |> Enum.map(&String.trim/1) |> 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) + [_, name] = Regex.run(~r/^\$?(\w+)$/, param) {:label, name} 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 + @doc """ + Assembles the given instruction and adds it to the memory (in reverse order). + """ def assemble_instruction(insn, memory \\ []) + @doc """ + Assemble add instruction. + + ## Examples + iex> Assembler.assemble_instruction("add 1, 2, 3") + [3, 2, 1, 1101] + """ def assemble_instruction("add " <> params, memory) do {[a, b, dest], modes} = parse_params(params, [:read, :read, :write]) [dest, b, a, 1 + modes | memory] end + @doc """ + Assemble multiply instruction. + + ## Examples + iex> Assembler.assemble_instruction("mul 1, 2, 3") + [3, 2, 1, 1102] + """ def assemble_instruction("mul " <> params, memory) do {[a, b, dest], modes} = parse_params(params, [:read, :read, :write]) [dest, b, a, 2 + modes | memory] end + @doc """ + Assemble halt instruction. + + ## Examples + iex> Assembler.assemble_instruction("hlt") + [99] + """ def assemble_instruction("hlt", memory) do [99 | memory] end + @doc """ + Assemble input instruction. + + ## Examples + iex> Assembler.assemble_instruction("in 7") + [7, 3] + """ def assemble_instruction("in " <> params, memory) do {[dest], modes} = parse_params(params, [:write]) [dest, 3 + modes | memory] end + @doc """ + Assemble input instruction. + + ## Examples + iex> Assembler.assemble_instruction("out 7") + [7, 104] + iex> Assembler.assemble_instruction("out $7") + [7, 4] + """ def assemble_instruction("out " <> params, memory) do {[val], modes} = parse_params(params, [:read]) [val, 4 + modes | memory] end + @doc """ + Assemble jump-if-non-zero instruction. + + ## Examples + iex> Assembler.assemble_instruction("jnz 1, 2") + [2, 1, 105] + iex> Assembler.assemble_instruction("jnz $var, 2") + [2, {:label, "var"}, 5] + """ def assemble_instruction("jnz " <> params, memory) do {[target, dest], modes} = parse_params(params, [:read, :read]) [dest, target, 5 + modes | memory] end + @doc """ + Assemble jump-if-zero instruction. + + ## Examples + iex> Assembler.assemble_instruction("jez 1, 2") + [2, 1, 106] + iex> Assembler.assemble_instruction("jez $var, 2") + [2, {:label, "var"}, 6] + """ def assemble_instruction("jez " <> params, memory) do {[target, dest], modes} = parse_params(params, [:read, :read]) [dest, target, 6 + modes | memory] end + @doc """ + Assemble compare-less-than instruction. + + ## Examples + iex> Assembler.assemble_instruction("clt 1, 2, 3") + [3, 2, 1, 1107] + iex> Assembler.assemble_instruction("clt $1, $2, 3") + [3, 2, 1, 7] + """ def assemble_instruction("clt " <> params, memory) do {[a, b, dest], modes} = parse_params(params, [:read, :read, :write]) [dest, b, a, 7 + modes | memory] end + @doc """ + Assemble compare-equal instruction. + + ## Examples + iex> Assembler.assemble_instruction("ceq 1, 2, 3") + [3, 2, 1, 1108] + iex> Assembler.assemble_instruction("ceq $1, $2, 3") + [3, 2, 1, 8] + """ def assemble_instruction("ceq " <> params, memory) do {[a, b, dest], modes} = parse_params(params, [:read, :read, :write]) diff --git a/lib/intcode/expression_evaluator.ex b/lib/intcode/expression_evaluator.ex new file mode 100644 index 0000000..5437d7c --- /dev/null +++ b/lib/intcode/expression_evaluator.ex @@ -0,0 +1,98 @@ +defmodule ExpressionEvaluator do + import NimbleParsec + + number = integer(min: 1) + + variable = ascii_string([?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 \\ %{}) do + {:ok, expr, _, _, _, _} = parse(expr) + do_eval(expr, vars) + end + + def do_eval([{op, [a, b]}], vars) do + do_op(op, do_eval_single(a, vars), do_eval_single(b, vars)) + end + + def do_eval([it], vars) do + do_eval_single(it, vars) + end + + def do_eval_single({:neg, [it]}, vars) do + -1 * do_eval_single(it, vars) + end + + def do_eval_single(it, _) when is_integer(it) do + it + end + + def do_eval_single(it, vars) when is_binary(it) do + Map.fetch!(vars, it) + end + + def do_eval_single(it, vars) do + do_eval([it], vars) + 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 diff --git a/mix.exs b/mix.exs index 5a0355a..3f6d185 100644 --- a/mix.exs +++ b/mix.exs @@ -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"} ] diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..d0046aa --- /dev/null +++ b/mix.lock @@ -0,0 +1,3 @@ +%{ + "nimble_parsec": {:hex, :nimble_parsec, "0.5.2", "1d71150d5293d703a9c38d4329da57d3935faed2031d64bc19e77b654ef2d177", [:mix], [], "hexpm"}, +} diff --git a/test/intcode/assembler_test.exs b/test/intcode/assembler_test.exs new file mode 100644 index 0000000..83b316b --- /dev/null +++ b/test/intcode/assembler_test.exs @@ -0,0 +1,79 @@ +defmodule AssemblerTest do + use ExUnit.Case + doctest Assembler + + test "assembles empty program" do + assert Assembler.assemble("") == [] + end + + test "assembles simple program" do + program = """ + add 1, 2, 10 + mul 2, 3, 11 + add $10, $11, 12 + hlt + """ + + assert Assembler.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 Assembler.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 Assembler.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 Assembler.assemble(program) == [1101, 1, 2, 3] + end +end diff --git a/test/intcode/expr_test.exs b/test/intcode/expr_test.exs new file mode 100644 index 0000000..4e9e7d3 --- /dev/null +++ b/test/intcode/expr_test.exs @@ -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