clone url: git://git.m455.casa/blep
blep.exs
1 | #!/usr/bin/env elixir |
2 |
|
3 | # heavily heavily referenced and studied the following material: |
4 | # - https://norvig.com/lispy.html |
5 | # - https://github.com/farazhaider/lispex/tree/master/lib |
6 | # - https://danthedev.com/lisp-in-your-language/ |
7 | # - https://mattbruv.github.io/mplisp/ |
8 | # - https://pages.cs.wisc.edu/~horwitz/CS704-NOTES/4.LISP-INTERP.html |
9 |
|
10 | defmodule Scopes do |
11 | def core do |
12 | %{ |
13 | := => fn(args) -> Enum.reduce(args, fn(n, acc) -> acc == n end) end, |
14 | :num? => fn([arg | _]) -> is_number(arg) end, |
15 | :empty? => fn([arg | _]) -> Enum.empty?(arg) end, |
16 | :list? => fn([arg | _]) -> is_list(arg) end, |
17 | :str? => fn([arg | _]) -> is_binary(arg) end, |
18 | :zero? => fn([arg | _]) -> arg == 0 end, |
19 | :inc => fn([arg | _]) -> arg + 1 end, |
20 | :dec => fn([arg | _]) -> arg - 1 end, |
21 | :do => fn(args) -> List.last(args) end, |
22 | :list => fn(args) -> args end, |
23 | :cons => fn([x, y]) -> [x | y] end, |
24 | :first => fn([arg | _]) -> hd(arg) end, |
25 | :rest => fn([arg | _]) -> tl(arg) end, |
26 | :str => fn(args) -> |
27 | args_as_strings = Enum.map(args, fn(arg) -> to_string(arg) end) |
28 | # TODO: maybe an enum.join here instead? |
29 | Enum.reduce(args_as_strings, fn(str, acc) -> acc <> str end) |
30 | end, |
31 | :num => fn([arg | _]) -> |
32 | try do |
33 | String.to_integer(arg) |
34 | rescue |
35 | ArgumentError -> String.to_float(arg) |
36 | end |
37 | end, |
38 | :print => fn([arg | _]) -> IO.write(arg) end, |
39 | String.to_atom("str->list") => fn([arg | _]) -> |
40 | String.graphemes(arg) |
41 | end, |
42 | String.to_atom("file->str") => fn([arg | _]) -> |
43 | case File.read(arg) do |
44 | {:ok, data} -> data |
45 | {:error, reason} -> |
46 | raise("oh shit: #{reason}") |
47 | end |
48 | end, |
49 | String.to_atom("str->data") => fn([arg | _]) -> |
50 | arg |
51 | |> String.graphemes() |
52 | |> Tokenization.tokenize([]) |
53 | |> Reading.read([]) |
54 | end, |
55 | } |
56 | end |
57 |
|
58 | def get(scope, key) do |
59 | case {scope[key], scope[:parent_scope]} do |
60 | {nil, nil} -> |
61 | raise("oh shit: couldn't find identifier: #{Atom.to_string(key)}") |
62 | {nil, parent_scope} -> get(parent_scope, key) |
63 | {local_value, _} -> local_value |
64 | end |
65 | end |
66 | end |
67 |
|
68 | defmodule Tokenization do |
69 | defp is_valid_atom(string) do |
70 | !Enum.member?([")", "(", "]", "[", "\s", "\n", "\r", "\t"], string) |
71 | end |
72 |
|
73 | defp atomize(string) do |
74 | case string do |
75 | "true" -> true |
76 | "fuck" -> false |
77 | _ -> |
78 | try do |
79 | String.to_integer(string) |
80 | rescue |
81 | ArgumentError -> |
82 | try do |
83 | String.to_float(string) |
84 | rescue |
85 | ArgumentError -> |
86 | String.to_atom(string) |
87 | end |
88 | end |
89 | end |
90 | end |
91 |
|
92 | defp ignore_line([char | rest]) do |
93 | if char !== "\n" and char !== "\r" do |
94 | ignore_line(rest) |
95 | else |
96 | rest |
97 | end |
98 | end |
99 |
|
100 | defp build_string([char | rest], string) do |
101 | if char !== "\"" do |
102 | build_string(rest, string <> char) |
103 | else |
104 | {rest, Macro.unescape_string(string)} |
105 | end |
106 | end |
107 |
|
108 | defp build_atom(graphemes, string, mode) do |
109 | [char | rest] = graphemes |
110 | if is_valid_atom(char) do |
111 | build_atom(rest, string <> char, mode) |
112 | else |
113 | # don't return rest here, so we don't lose the last character |
114 | result = if mode == :keyword, do: string, else: atomize(string) |
115 | {graphemes, result} |
116 | end |
117 | end |
118 |
|
119 | def tokenize(graphemes, tokens) do |
120 | if Enum.empty?(graphemes) do |
121 | Enum.reverse(tokens) |
122 | else |
123 | [char | rest] = graphemes |
124 |
|
125 | case char do |
126 | # elixir doesn't support definitions in when clauses, only macros, which |
127 | # i haven't learned yet in elixir lol |
128 | c when c == "\s" or c == "\n" or c == "\r" or c == "\t" -> |
129 | tokenize(rest, tokens) |
130 |
|
131 | ";" -> |
132 | remaining_graphemes = ignore_line(rest) |
133 | tokenize(remaining_graphemes, tokens) |
134 |
|
135 | "\"" -> |
136 | {remaining_graphemes, built_string} = build_string(rest, "") |
137 | tokenize(remaining_graphemes, [built_string | tokens]) |
138 |
|
139 | ":" -> |
140 | {remaining_graphemes, built_literal} = build_atom(rest, "", :keyword) |
141 | tokenize(remaining_graphemes, [built_literal | tokens]) |
142 |
|
143 | "(" -> |
144 | tokenize(rest, [char | tokens]) |
145 |
|
146 | ")" -> |
147 | tokenize(rest, [char | tokens]) |
148 |
|
149 | "[" -> |
150 | tokenize(rest, [:list, char | tokens]) |
151 |
|
152 | "]" -> |
153 | tokenize(rest, [char | tokens]) |
154 |
|
155 | _ -> |
156 | {remaining_graphemes, built_atom} = build_atom(graphemes, "", :atom) |
157 | tokenize(remaining_graphemes, [built_atom | tokens]) |
158 | end |
159 | end |
160 | end |
161 | end |
162 |
|
163 | defmodule Reading do |
164 | def read(tokens, ast) do |
165 | if Enum.empty?(tokens) do |
166 | # Reverse this so the program evaluates from top to bottom of code |
167 | Enum.reverse(ast) |
168 | else |
169 | [token | rest] = tokens |
170 |
|
171 | cond do |
172 | token == "(" -> |
173 | {remaining_tokens, sub_list} = read(rest, []) |
174 | read(remaining_tokens, [sub_list | ast]) |
175 |
|
176 | token == "[" -> |
177 | {remaining_tokens, sub_list} = read(rest, []) |
178 | read(remaining_tokens, [sub_list | ast]) |
179 |
|
180 | token == ")" -> |
181 | {rest, Enum.reverse(ast)} |
182 |
|
183 | token == "]" -> |
184 | {rest, Enum.reverse(ast)} |
185 |
|
186 | true -> |
187 | read(rest, [token | ast]) |
188 | end |
189 | end |
190 | end |
191 | end |
192 |
|
193 | defmodule Evaluation do |
194 | defp eval_args(expression, current_scope) do |
195 | if Enum.empty?(expression) do |
196 | [] |
197 | else |
198 | [first | rest] = expression |
199 | {result, new_scope} = eval(first, current_scope) |
200 | [result | eval_args(rest, new_scope)] |
201 | end |
202 | end |
203 |
|
204 | defp eval_cond(pairs, current_scope) do |
205 | if Enum.empty?(pairs) do |
206 | # instead of returning an error, i return false for the programmer if they |
207 | # didn't specify a final [true result] condition |
208 | {false, current_scope} |
209 | else |
210 | [pair_cond, pair_result] = hd(pairs) |
211 | condition_result = eval(pair_cond, current_scope) |> elem(0) |
212 | if condition_result do |
213 | eval(pair_result, current_scope) |
214 | else |
215 | eval_cond(tl(pairs), current_scope) |
216 | end |
217 | end |
218 | end |
219 |
|
220 | defp is_definition?(x) do |
221 | is_atom(x) and |
222 | x != true and |
223 | x != false and |
224 | # so we don't try to first look for a :fn key in the global scope |
225 | x != :fn |
226 | end |
227 |
|
228 | def eval(input, current_scope) do |
229 | cond do |
230 | is_definition?(input) -> |
231 | {Scopes.get(current_scope, input), current_scope} |
232 |
|
233 | not is_list(input) -> |
234 | {input, current_scope} |
235 |
|
236 | hd(input) == :quote-> |
237 | {tl(input) |> hd, current_scope} |
238 |
|
239 | hd(input) == :eval-> |
240 | [_, expr] = input |
241 | # honestly not sure why i need to eval twice lol |
242 | {result, new_scope} = eval(expr, current_scope) |
243 | if is_list(expr) and hd(expr) == :quote do |
244 | # if we are evaluating a quoted expression, the result of the quoted |
245 | # expression is a literal value, so we need to then evaluate that |
246 | # result to get it to return a definition's value |
247 | eval(result, new_scope) |
248 | else |
249 | # otherwise, we just evaluate a regular argument |
250 | # ... maybe i should just error here, since you wouldn't need to |
251 | # evaluate a non-quoted expression? i dont even know |
252 | {result, new_scope} |
253 | end |
254 |
|
255 | hd(input) == :if-> |
256 | [_, user_cond, t_result, f_result] = input |
257 | cond_result = eval(user_cond, current_scope) |> elem(0) |
258 | if cond_result do |
259 | eval(t_result, current_scope) |
260 | else |
261 | eval(f_result, current_scope) |
262 | end |
263 |
|
264 | hd(input) == :cond -> |
265 | # [:cond |
266 | # [:list [:> 2 1] :yes] |
267 | # [:list [:< 2 1] :yes] |
268 | # [:list true :other]] |
269 | [_ | pairs] = input |
270 | # remove preceding :list from each pair |
271 | pairs_without_prefix = Enum.map(pairs, fn(pair) -> tl(pair) end) |
272 | eval_cond(pairs_without_prefix, current_scope) |
273 |
|
274 | hd(input) == :import -> |
275 | [_ | filenames] = input |
276 | new_scope = Enum.reduce(filenames, current_scope, fn(file, acc_scope) -> |
277 | case File.read(file) do |
278 | {:ok, data} -> |
279 | data |
280 | |> String.graphemes() |
281 | |> Tokenization.tokenize([]) |
282 | |> Reading.read([]) |
283 | |> Enum.reduce(acc_scope, fn(expr, s) -> |
284 | Evaluation.eval(expr, s) |> elem(1) end) |
285 | {:error, reason} -> |
286 | raise("oh shit: #{reason}") |
287 | end |
288 | end) |
289 | {:ok, new_scope} |
290 |
|
291 | hd(input) == :def -> |
292 | [_, symbol, body] = input |
293 | {body_evaluated, _} = eval(body, current_scope) |
294 | {:ok, Map.put(current_scope, symbol, body_evaluated)} |
295 |
|
296 | hd(input) == :func -> |
297 | [_, symbol, parameters | body] = input |
298 | eval([:def, symbol, [:fn, parameters, body]], current_scope) |
299 |
|
300 | # returns raw ast so we don't try to evaluate parameters that aren't |
301 | # mapped to arguments yet |
302 | hd(input) == :fn -> |
303 | [_, parameters | body] = input |
304 | # turn [:list, param_a, param_b] into [param_a, param_b] |
305 | parameters_list_prefix_removed = tl(parameters) |
306 | {{parameters_list_prefix_removed, body}, current_scope} |
307 |
|
308 | hd(input) == :let -> |
309 | # [:let [:list [:list a "aaa"] |
310 | # [:list b "aaa"]] |
311 | # ...] |
312 | [_, list_of_pairs | body] = input |
313 | # map on the tail so we don't include the first :list. |
314 | # return the tail of each pair so we don't need process the `:list`s |
315 | pairs = Enum.map(tl(list_of_pairs), fn(pair) -> tl(pair) end) |
316 | local_scope = Enum.reduce(pairs, %{:parent_scope => current_scope}, fn(pair, acc_scope) -> |
317 | [symbol, val] = pair |
318 | eval([:def, symbol, val], acc_scope) |> elem(1) |
319 | end) |
320 | result = eval(body, local_scope) |> elem(0) |
321 | {result, current_scope} |
322 |
|
323 | true -> |
324 | [head | tail] = input |
325 | {head_evaluated, _} = eval(head, current_scope) |
326 | args = eval_args(tail, Map.put(%{}, :parent_scope, current_scope)) |
327 | cond do |
328 | is_tuple(head_evaluated) -> |
329 | {parameters, body} = head_evaluated |
330 | # create something like %{:p1 => arg1, :p2 => arg2} |
331 | parameters_map = Enum.zip(parameters, args) |> Enum.into(%{}) |
332 | # evaluate the body of the anonymous function using a local scope to |
333 | # populate the originally undefined values in the anonymous |
334 | # function's body |
335 | {body_evaluated, _} = eval(body, Map.put(parameters_map, :parent_scope, current_scope)) |
336 | {body_evaluated, current_scope} |
337 | is_function(head_evaluated) -> |
338 | {head_evaluated.(args), current_scope} |
339 | true -> |
340 | # this /seems/ to be the return value of a function but i could be |
341 | # wrong? |
342 | {head_evaluated, current_scope} |
343 | end |
344 | end |
345 | end |
346 | end |
347 |
|
348 | defmodule Main do |
349 | def lisp_string(expression) do |
350 | cond do |
351 | expression == false -> |
352 | "fuck" |
353 | is_atom(expression) and expression != true -> |
354 | ":" <> to_string(expression) |
355 | is_binary(expression) -> "\"" <> expression <> "\"" |
356 | # still unsure about whether or not i want to show :list in a list when |
357 | # quoted at the repl like so: [:list 1 2 :thing] |
358 | is_list(expression) -> |
359 | expression_list_prefix_removed = Enum.filter(expression, fn(expr) -> |
360 | expr != :list end) |
361 | list_of_strings = Enum.map(expression_list_prefix_removed, fn (expr) -> |
362 | lisp_string(expr) |
363 | end) |
364 | "[" <> Enum.join(list_of_strings, " ") <> "]" |
365 | true -> |
366 | to_string(expression) |
367 | end |
368 | end |
369 |
|
370 | defp repl(scope) do |
371 | input = IO.gets("> ") |
372 | # lol why evaluate anything when i can do something awful like this |
373 | cond do |
374 | input == "exit\n" or |
375 | input == "(exit)\n" or |
376 | input == "quit\n" or |
377 | input == "(quit)\n" or |
378 | input == "q\n" -> |
379 | exit(:normal) |
380 | input == :eof -> |
381 | IO.puts("") |
382 | exit(:normal) |
383 | true -> |
384 | {result, new_scope} = input |
385 | |> String.graphemes() |
386 | |> Tokenization.tokenize([]) |
387 | |> Reading.read([]) |
388 | |> Enum.reduce({nil, scope}, fn(expr, acc_tuple) -> |
389 | Evaluation.eval(expr, elem(acc_tuple, 1)) end) |
390 | IO.puts(lisp_string(result)) |
391 | repl(new_scope) |
392 | end |
393 | end |
394 |
|
395 | def handle_args(args) do |
396 | scope = Scopes.core |
397 | if length(args) == 0 do |
398 | IO.puts("welcome to blep, the shitty lisp") |
399 | IO.puts("") |
400 | repl(scope) |
401 | else |
402 | case File.read(hd(args)) do |
403 | {:ok, data} -> |
404 | data |
405 | |> String.graphemes() |
406 | |> Tokenization.tokenize([]) |
407 | |> Reading.read([]) |
408 | |> Enum.reduce({nil, scope}, fn(expr, acc_tuple) -> |
409 | Evaluation.eval(expr, elem(acc_tuple, 1)) end) |
410 | {:error, reason} -> |
411 | raise("oh shit: #{reason}") |
412 | end |
413 | end |
414 | end |
415 | end |
416 |
|
417 | Main.handle_args(System.argv()) |