Change quasiquote algorithm
[jackhill/mal.git] / impls / elixir / lib / mix / tasks / step7_quote.ex
1 defmodule Mix.Tasks.Step7Quote do
2 import Mal.Types
3 alias Mal.Function
4
5 def run(args) do
6 env = Mal.Env.new()
7 Mal.Env.merge(env, Mal.Core.namespace)
8 bootstrap(args, env)
9 loop(env)
10 end
11
12 defp load_file(file_name, env) do
13 read_eval_print("""
14 (load-file "#{file_name}")
15 """, env)
16 exit(:normal)
17 end
18
19 defp bootstrap(args, env) do
20 # not:
21 read_eval_print("""
22 (def! not
23 (fn* (a) (if a false true)))
24 """, env)
25
26 # load-file:
27 read_eval_print("""
28 (def! load-file
29 (fn* (f)
30 (eval (read-string (str "(do " (slurp f) "\nnil)")))))
31 """, env)
32
33 Mal.Env.set(env, "eval", %Function{value: fn [ast] ->
34 eval(ast, env)
35 end})
36
37 case args do
38 [file_name | rest] ->
39 Mal.Env.set(env, "*ARGV*", list(rest))
40 load_file(file_name, env)
41
42 [] ->
43 Mal.Env.set(env, "*ARGV*", list([]))
44 end
45 end
46
47 defp loop(env) do
48 IO.write(:stdio, "user> ")
49 IO.read(:stdio, :line)
50 |> read_eval_print(env)
51 |> IO.puts
52
53 loop(env)
54 end
55
56 defp eval_ast({:list, ast, meta}, env) when is_list(ast) do
57 {:list, Enum.map(ast, fn elem -> eval(elem, env) end), meta}
58 end
59
60 defp eval_ast({:map, ast, meta}, env) do
61 map = for {key, value} <- ast, into: %{} do
62 {eval(key, env), eval(value, env)}
63 end
64
65 {:map, map, meta}
66 end
67
68 defp eval_ast({:vector, ast, meta}, env) do
69 {:vector, Enum.map(ast, fn elem -> eval(elem, env) end), meta}
70 end
71
72 defp eval_ast({:symbol, symbol}, env) do
73 case Mal.Env.get(env, symbol) do
74 {:ok, value} -> value
75 :not_found -> throw({:error, "'#{symbol}' not found"})
76 end
77 end
78
79 defp eval_ast(ast, _env), do: ast
80
81 defp read(input) do
82 Mal.Reader.read_str(input)
83 end
84
85 defp eval_bindings([], env), do: env
86 defp eval_bindings([{:symbol, key}, binding | tail], env) do
87 evaluated = eval(binding, env)
88 Mal.Env.set(env, key, evaluated)
89 eval_bindings(tail, env)
90 end
91 defp eval_bindings(_bindings, _env), do: throw({:error, "Unbalanced let* bindings"})
92
93 defp quasiquote({:list, [{:symbol, "unquote"}, arg], _}), do: arg
94 defp quasiquote({:list, [{:symbol, "unquote"}| _], _}), do: throw({:error, "unquote: arg count"})
95 defp quasiquote({:list, xs, _}), do: qq_foldr(xs)
96 defp quasiquote({:vector, xs, _}), do: list([{:symbol, "vec"}, qq_foldr(xs)])
97 defp quasiquote({:symbol, sym}), do: list([{:symbol, "quote"}, {:symbol, sym}])
98 defp quasiquote({:map, ast, meta}), do: list([{:symbol, "quote"}, {:map, ast, meta}])
99 defp quasiquote(ast), do: ast
100
101 defp qq_foldr([]), do: list([])
102 defp qq_foldr([x|xs]), do: qq_loop(x, qq_foldr xs)
103
104 defp qq_loop({:list, [{:symbol, "splice-unquote"}, arg], _}, acc), do: list([{:symbol, "concat"}, arg, acc])
105 defp qq_loop({:list, [{:symbol, "splice-unquote"}| _], _}, _), do: throw({:error, "splice-unquote: arg count"})
106 defp qq_loop(elt, acc), do: list([{:symbol, "cons"}, quasiquote(elt), acc])
107
108 defp eval({:list, [], _} = empty_ast, _env), do: empty_ast
109 defp eval({:list, ast, meta}, env), do: eval_list(ast, env, meta)
110 defp eval(ast, env), do: eval_ast(ast, env)
111
112 defp eval_list([{:symbol, "if"}, condition, if_true | if_false], env, _) do
113 result = eval(condition, env)
114 if result == nil or result == false do
115 case if_false do
116 [] -> nil
117 [body] -> eval(body, env)
118 end
119 else
120 eval(if_true, env)
121 end
122 end
123
124 defp eval_list([{:symbol, "do"} | ast], env, _) do
125 ast
126 |> List.delete_at(-1)
127 |> list
128 |> eval_ast(env)
129 eval(List.last(ast), env)
130 end
131
132 defp eval_list([{:symbol, "def!"}, {:symbol, key}, value], env, _) do
133 evaluated = eval(value, env)
134 Mal.Env.set(env, key, evaluated)
135 evaluated
136 end
137
138 defp eval_list([{:symbol, "let*"}, {list_type, bindings, _}, body], env, _)
139 when list_type == :list or list_type == :vector do
140 let_env = Mal.Env.new(env)
141 eval_bindings(bindings, let_env)
142 eval(body, let_env)
143 end
144
145 defp eval_list([{:symbol, "fn*"}, {list_type, params, _}, body], env, _)
146 when list_type == :list or list_type == :vector do
147 param_symbols = for {:symbol, symbol} <- params, do: symbol
148
149 closure = fn args ->
150 inner = Mal.Env.new(env, param_symbols, args)
151 eval(body, inner)
152 end
153
154 %Function{value: closure}
155 end
156
157 defp eval_list([{:symbol, "quote"}, arg], _env, _), do: arg
158
159 defp eval_list([{:symbol, "quasiquoteexpand"}, ast], _, _) do
160 quasiquote(ast)
161 end
162
163 defp eval_list([{:symbol, "quasiquote"}, ast], env, _) do
164 ast |> quasiquote
165 |> eval(env)
166 end
167
168 defp eval_list(ast, env, meta) do
169 {:list, [func | args], _} = eval_ast({:list, ast, meta}, env)
170 func.value.(args)
171 end
172
173 defp print(value) do
174 Mal.Printer.print_str(value)
175 end
176
177 defp read_eval_print(:eof, _env), do: exit(:normal)
178 defp read_eval_print(line, env) do
179 read(line)
180 |> eval(env)
181 |> print
182 catch
183 {:error, message} -> IO.puts("Error: #{message}")
184 end
185 end