Add Nimble Parsed based arithmetic expression evaluator
This commit is contained in:
parent
0f5e7f7907
commit
49b2a79d0a
|
@ -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,54 +47,108 @@ 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
|
||||
|> Enum.reverse()
|
||||
|> Enum.with_index()
|
||||
|> Enum.map(fn
|
||||
{{:label, name}, index} -> get_label(name, index, labels)
|
||||
{it, _} -> it
|
||||
end)
|
||||
end
|
||||
|
||||
IO.inspect(labels)
|
||||
Enum.reverse(memory)
|
||||
@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 repeat(_, 0), do: []
|
||||
def repeat(n, count), do: [n | repeat(n, count - 1)]
|
||||
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)
|
||||
case type do
|
||||
:read ->
|
||||
if String.starts_with?(param, "$") do
|
||||
modes
|
||||
else
|
||||
modes + pow(10, index + 2)
|
||||
end
|
||||
|
||||
_ ->
|
||||
modes
|
||||
end
|
||||
|
||||
|
@ -99,54 +156,130 @@ defmodule Assembler do
|
|||
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])
|
||||
|
||||
|
|
|
@ -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
|
1
mix.exs
1
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"}
|
||||
]
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
%{
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "0.5.2", "1d71150d5293d703a9c38d4329da57d3935faed2031d64bc19e77b654ef2d177", [:mix], [], "hexpm"},
|
||||
}
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue