From d2af80fa2632ac938f7fb45815bf10a406edc82f Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 7 Dec 2019 19:00:46 -0500 Subject: [PATCH] Intcode assembler: replace parser with Nimble Parsec --- lib/day5/day5.ex | 4 +- lib/intcode/assembler.ex | 327 +++++++++++----------------- lib/intcode/expression_evaluator.ex | 34 +-- test/intcode/assembler_test.exs | 43 +++- 4 files changed, 194 insertions(+), 214 deletions(-) diff --git a/lib/day5/day5.ex b/lib/day5/day5.ex index 7f1ef53..62d4b16 100644 --- a/lib/day5/day5.ex +++ b/lib/day5/day5.ex @@ -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 diff --git a/lib/intcode/assembler.ex b/lib/intcode/assembler.ex index af13bfb..4955107 100644 --- a/lib/intcode/assembler.ex +++ b/lib/intcode/assembler.ex @@ -1,7 +1,45 @@ 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 res - clt $res, 8 cmpRes + clt $res, _self + 1, cmpRes jnz $cmpRes, lessThan ceq $res, 8, cmpRes jnz $cmpRes, equal @@ -20,77 +58,105 @@ defmodule Assembler do cmpRes: 0 """ def test do - @asm - |> IO.inspect() - |> assemble() + assemble(@asm) end 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)) - } - - 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}") + err -> + raise "Unable to parse line: '#{line}':\n#{inspect(err)}" end end) - - # IO.inspect(labels) + |> Enum.reduce({[], %{}}, &assemble_line/2) memory |> Enum.reverse() |> Enum.with_index() |> Enum.map(fn - {{:label, name}, index} -> get_label(name, index, labels) - {it, _} -> it + {{:expr, expr}, index} -> + ExpressionEvaluator.eval(expr, fn + "_self" -> index + name -> Map.fetch!(labels, name) + end) + + {it, _} -> + it end) end - @doc """ - Get the address of a label. + def assemble_line([], acc), do: acc - `_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 + def assemble_line([label: [name]], {memory, labels}) do + { + memory, + Map.put(labels, name, length(memory)) + } end - def get_label(name, _, labels) do - Map.fetch!(labels, name) + 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 """ @@ -109,23 +175,21 @@ defmodule Assembler do Parses assembly instruction parameter values and modes. ## Examples - iex> Assembler.parse_params("1, 2, 3", [:read, :read, :read]) + iex> Assembler.parse_params(["1", "2", "3"], [:read, :read, :read]) {[1, 2, 3], 11100} - iex> Assembler.parse_params("1, 2, 3", [:read, :read, :write]) + iex> Assembler.parse_params(["1", "2", "3"], [:read, :read, :write]) {[1, 2, 3], 1100} - iex> Assembler.parse_params("1, 2, $3", [:read, :read, :write]) + iex> Assembler.parse_params(["1", "2", "$3"], [:read, :read, :write]) {[1, 2, 3], 1100} - iex> Assembler.parse_params("$1, 2, 3", [:read, :read, :write]) + 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} + 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.map(&String.trim/1) |> Enum.zip(param_types) |> Enum.with_index() |> Enum.map_reduce(0, fn {{param, type}, index}, modes -> @@ -135,8 +199,13 @@ defmodule Assembler do String.to_integer(digits) _ -> - [_, name] = Regex.run(~r/^\$?(\w+)$/, param) - {:label, name} + case param do + "$" <> param -> + {:expr, param} + + _ -> + {:expr, param} + end end modes = @@ -155,134 +224,4 @@ defmodule Assembler do {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]) - - [dest, b, a, 8 + modes | memory] - end end diff --git a/lib/intcode/expression_evaluator.ex b/lib/intcode/expression_evaluator.ex index 5437d7c..2c05989 100644 --- a/lib/intcode/expression_evaluator.ex +++ b/lib/intcode/expression_evaluator.ex @@ -3,7 +3,7 @@ defmodule ExpressionEvaluator do number = integer(min: 1) - variable = ascii_string([?a..?z, ?_], min: 1) + variable = ascii_string([?a..?z, ?A..?Z, ?_], min: 1) whitespace = ascii_string([?\s], min: 1) @@ -62,33 +62,41 @@ defmodule ExpressionEvaluator do defparsec(:parse, parsec(:expr)) - def eval(expr, vars \\ %{}) do + 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, vars) + do_eval(expr, get_var) end - def do_eval([{op, [a, b]}], vars) do - do_op(op, do_eval_single(a, vars), do_eval_single(b, vars)) + 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], vars) do - do_eval_single(it, vars) + def do_eval([it], get_var) do + do_eval_single(it, get_var) end - def do_eval_single({:neg, [it]}, vars) do - -1 * do_eval_single(it, vars) + 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, vars) when is_binary(it) do - Map.fetch!(vars, it) + def do_eval_single(it, get_var) when is_binary(it) do + get_var.(it) end - def do_eval_single(it, vars) do - do_eval([it], vars) + def do_eval_single(it, get_var) do + do_eval([it], get_var) end def do_op(:add, a, b), do: a + b diff --git a/test/intcode/assembler_test.exs b/test/intcode/assembler_test.exs index 83b316b..a757d13 100644 --- a/test/intcode/assembler_test.exs +++ b/test/intcode/assembler_test.exs @@ -1,9 +1,27 @@ defmodule AssemblerTest do use ExUnit.Case doctest Assembler + import Assembler test "assembles empty program" do - assert Assembler.assemble("") == [] + 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 @@ -14,7 +32,7 @@ defmodule AssemblerTest do hlt """ - assert Assembler.assemble(program) == [ + assert assemble(program) == [ 1101, 1, 2, @@ -39,7 +57,7 @@ defmodule AssemblerTest do hlt """ - assert Assembler.assemble(program) == [ + assert assemble(program) == [ 1101, 1, 2, @@ -58,7 +76,7 @@ defmodule AssemblerTest do var: 0 """ - assert Assembler.assemble(program) == [ + assert assemble(program) == [ 1101, 1, 2, @@ -74,6 +92,21 @@ defmodule AssemblerTest do add 1, 2, _self """ - assert Assembler.assemble(program) == [1101, 1, 2, 3] + 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