diff --git a/elixir/lib/mal/printer.ex b/elixir/lib/mal/printer.ex new file mode 100644 index 00000000..57879273 --- /dev/null +++ b/elixir/lib/mal/printer.ex @@ -0,0 +1,13 @@ +defmodule Mal.Printer do + def print_str(mal) when is_atom(mal), do: Atom.to_string(mal) + def print_str(mal) when is_integer(mal), do: Integer.to_string(mal) + def print_str(mal) when is_bitstring(mal), do: mal + def print_str({ :symbol, value }), do: value + def print_str(mal) when is_list(mal) do + output = mal + |> Enum.map(fn(x) -> print_str(x) end) + |> Enum.join(" ") + + "(#{output})" + end +end diff --git a/elixir/lib/mal/reader.ex b/elixir/lib/mal/reader.ex new file mode 100644 index 00000000..46144bb3 --- /dev/null +++ b/elixir/lib/mal/reader.ex @@ -0,0 +1,58 @@ +# TODO: def -> defp for everything but read_str +defmodule Mal.Reader do + import Mal.Types + + def read_str(input) do + output = tokenize(input) + |> read_form + |> elem(0) + {:ok, output} + catch + {:invalid, message} -> {:error, message} + end + + def tokenize(input) do + regex = ~r/[\s,]*(~@|[\[\]{}()'`~^@]|"(?:\\.|[^\\"])*"|;.*|[^\s\[\]{}('"`,;)]*)/ + Regex.scan(regex, input, capture: :all_but_first) + |> List.flatten + |> List.delete_at(-1) # Remove the last match, which is an empty string + end + + def read_form([next | rest] = tokens) do + case next do + "(" <> _ -> + read_list(tokens) + _ -> + token = read_atom(next) + {token, rest} + end + end + + def read_list([_ | tokens]), do: do_read_list(tokens, []) + + defp do_read_list([], acc), do: throw({:invalid, "expected ')', got EOF"}) + defp do_read_list([head | tail] = tokens, acc) do + case head do + ")" <> _ -> {Enum.reverse(acc), tail} + _ -> + {token, rest} = read_form(tokens) + do_read_list(rest, [token | acc]) + end + end + + def read_atom("nil"), do: nil + def read_atom("true"), do: true + def read_atom("false"), do: false + def read_atom(":" <> rest), do: String.to_atom(rest) + def read_atom(token) do + cond do + String.starts_with?(token, "\"") -> token + String.starts_with?(token, "'") -> token + integer?(token) -> + Integer.parse(token) + |> elem(0) + + true -> {:symbol, token} + end + end +end diff --git a/elixir/lib/mal/types.ex b/elixir/lib/mal/types.ex new file mode 100644 index 00000000..6c27e299 --- /dev/null +++ b/elixir/lib/mal/types.ex @@ -0,0 +1,9 @@ +defmodule Mal.Types do + def integer?(input) do + Regex.match?(~r/^-?[0-9]+$/, input) + end + + def float?(input) do + Regex.match?(~r/^-?[0-9][0-9.]*$/, input) + end +end diff --git a/elixir/lib/mix/tasks/step1_read_print.ex b/elixir/lib/mix/tasks/step1_read_print.ex new file mode 100644 index 00000000..8785332f --- /dev/null +++ b/elixir/lib/mix/tasks/step1_read_print.ex @@ -0,0 +1,32 @@ +defmodule Mix.Tasks.Step1ReadPrint do + def run(_), do: main + + def main do + IO.write(:stdio, "user> ") + IO.read(:stdio, :line) + |> read_eval_print + + main + end + + def read(input) do + Mal.Reader.read_str(input) + end + + def eval({:ok, input}), do: {:ok, input} + def eval({:error, message}), do: {:error, message} + + def print({:ok, output}) do + IO.puts(Mal.Printer.print_str(output)) + end + def print({:error, message}) do + IO.puts(message) + end + + def read_eval_print(:eof), do: exit(0) + def read_eval_print(line) do + read(line) + |> eval + |> print + end +end