All: don't ignore */mal. Fixes #99
[jackhill/mal.git] / elixir / lib / mal / reader.ex
1 defmodule Mal.Reader do
2 import Mal.Types
3
4 def read_str(input) do
5 case tokenize(input) do
6 [] -> nil
7 tokens -> tokens
8 |> read_form
9 |> elem(0)
10 end
11 end
12
13 def tokenize(input) do
14 regex = ~r/[\s,]*(~@|[\[\]{}()'`~^@]|"(?:\\.|[^\\"])*"|;.*|[^\s\[\]{}('"`,;)]*)/
15 Regex.scan(regex, input, capture: :all_but_first)
16 |> List.flatten
17 |> List.delete_at(-1) # Remove the last match, which is an empty string
18 |> Enum.filter(fn token -> not String.starts_with?(token, ";") end)
19 end
20
21 defp read_form([next | rest] = tokens) do
22 case next do
23 "(" -> read_list(tokens)
24 "[" -> read_vector(tokens)
25 "{" -> read_hash_map(tokens)
26 "'" -> create_quote("quote", rest)
27 "`" -> create_quote("quasiquote", rest)
28 "~" -> create_quote("unquote", rest)
29 "~@" -> create_quote("splice-unquote", rest)
30 "@" -> create_quote("deref", rest)
31 "^" -> create_meta(rest)
32 ")" -> throw({:error, "unexpected )"})
33 "]" -> throw({:error, "unexpected ]"})
34 "}" -> throw({:error, "unexpected }"})
35 _ ->
36 token = read_atom(next)
37 {token, rest}
38 end
39 end
40
41 defp create_meta(tokens) do
42 {meta, meta_rest} = read_form(tokens)
43 {token, rest_tokens} = read_form(meta_rest)
44 new_token = list([{:symbol, "with-meta"}, token, meta])
45 {new_token, rest_tokens}
46 end
47
48 defp create_quote(quote_type, tokens) do
49 {token, rest_tokens} = read_form(tokens)
50 new_token = list([{:symbol, quote_type}, token])
51 {new_token, rest_tokens}
52 end
53
54 defp read_list([_ | tokens]) do
55 {ast, rest} = do_read_sequence(tokens, [], "(", ")")
56 {list(ast), rest}
57 end
58
59 defp read_vector([_ | tokens]) do
60 {ast, rest} = do_read_sequence(tokens, [], "[", "]")
61 {vector(ast), rest}
62 end
63
64 defp read_hash_map([_ | tokens]) do
65 {map, rest} = do_read_sequence(tokens, [], "{", "}")
66 {hash_map(map), rest}
67 end
68
69 defp do_read_sequence([], _acc, _start_sep, end_sep), do: throw({:error, "expected #{end_sep}, got EOF"})
70 defp do_read_sequence([head | tail] = tokens, acc, start_sep, end_sep) do
71 cond do
72 String.starts_with?(head, end_sep) ->
73 {Enum.reverse(acc), tail}
74 true ->
75 {token, rest} = read_form(tokens)
76 do_read_sequence(rest, [token | acc], start_sep, end_sep)
77 end
78 end
79
80 defp read_atom("nil"), do: nil
81 defp read_atom("true"), do: true
82 defp read_atom("false"), do: false
83 defp read_atom(":" <> rest), do: String.to_atom(rest)
84 defp read_atom(token) do
85 cond do
86 String.starts_with?(token, "\"") and String.ends_with?(token, "\"") ->
87 token
88 |> String.slice(1..-2)
89 |> String.replace("\\\"", "\"")
90
91 integer?(token) ->
92 Integer.parse(token)
93 |> elem(0)
94
95 true -> {:symbol, token}
96 end
97 end
98 end