Intcode assembler: replace parser with Nimble Parsec
This commit is contained in:
parent
49b2a79d0a
commit
d2af80fa26
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue