From 75eca2ca5b5b450b36f73bcddbdad1d921c19c3e Mon Sep 17 00:00:00 2001 From: Alessandro Cerruti Date: Thu, 12 Feb 2026 22:38:41 +0100 Subject: [PATCH] elixir.2: provide new elixir implementation --- IMPLS.yml | 1 + Makefile.impls | 3 +- README.md | 14 +- impls/elixir.2/.credo.exs | 13 + impls/elixir.2/.formatter.exs | 4 + impls/elixir.2/.gitignore | 25 + impls/elixir.2/.iex.exs | 1 + impls/elixir.2/Dockerfile | 28 + impls/elixir.2/Makefile | 14 + impls/elixir.2/lib/mal.ex | 21 + impls/elixir.2/lib/mal/core.ex | 513 ++++++++++++++++++ .../elixir.2/lib/mal/empty_statement_error.ex | 6 + impls/elixir.2/lib/mal/env.ex | 88 +++ impls/elixir.2/lib/mal/env/instance.ex | 54 ++ impls/elixir.2/lib/mal/eval_error.ex | 3 + .../lib/mal/exception_wrapper_error.ex | 14 + impls/elixir.2/lib/mal/parse_error.ex | 3 + impls/elixir.2/lib/mal/printer.ex | 79 +++ impls/elixir.2/lib/mal/reader.ex | 187 +++++++ impls/elixir.2/lib/mal/type.ex | 26 + impls/elixir.2/lib/mal/type/function.ex | 15 + impls/elixir.2/lib/steps/step0_repl.ex | 40 ++ impls/elixir.2/lib/steps/step1_read_print.ex | 53 ++ impls/elixir.2/lib/steps/step2_eval.ex | 116 ++++ impls/elixir.2/lib/steps/step3_env.ex | 168 ++++++ impls/elixir.2/lib/steps/step4_if_fn_do.ex | 211 +++++++ impls/elixir.2/lib/steps/step5_tco.ex | 238 ++++++++ impls/elixir.2/lib/steps/step6_file.ex | 259 +++++++++ impls/elixir.2/lib/steps/step7_quote.ex | 269 +++++++++ impls/elixir.2/lib/steps/step8_macros.ex | 297 ++++++++++ impls/elixir.2/lib/steps/step9_try.ex | 333 ++++++++++++ impls/elixir.2/lib/steps/stepA_mal.ex | 338 ++++++++++++ impls/elixir.2/mix.exs | 42 ++ impls/elixir.2/mix.lock | 8 + impls/elixir.2/run | 5 + 35 files changed, 3487 insertions(+), 2 deletions(-) create mode 100644 impls/elixir.2/.credo.exs create mode 100644 impls/elixir.2/.formatter.exs create mode 100644 impls/elixir.2/.gitignore create mode 100644 impls/elixir.2/.iex.exs create mode 100644 impls/elixir.2/Dockerfile create mode 100644 impls/elixir.2/Makefile create mode 100644 impls/elixir.2/lib/mal.ex create mode 100644 impls/elixir.2/lib/mal/core.ex create mode 100644 impls/elixir.2/lib/mal/empty_statement_error.ex create mode 100644 impls/elixir.2/lib/mal/env.ex create mode 100644 impls/elixir.2/lib/mal/env/instance.ex create mode 100644 impls/elixir.2/lib/mal/eval_error.ex create mode 100644 impls/elixir.2/lib/mal/exception_wrapper_error.ex create mode 100644 impls/elixir.2/lib/mal/parse_error.ex create mode 100644 impls/elixir.2/lib/mal/printer.ex create mode 100644 impls/elixir.2/lib/mal/reader.ex create mode 100644 impls/elixir.2/lib/mal/type.ex create mode 100644 impls/elixir.2/lib/mal/type/function.ex create mode 100644 impls/elixir.2/lib/steps/step0_repl.ex create mode 100644 impls/elixir.2/lib/steps/step1_read_print.ex create mode 100644 impls/elixir.2/lib/steps/step2_eval.ex create mode 100644 impls/elixir.2/lib/steps/step3_env.ex create mode 100644 impls/elixir.2/lib/steps/step4_if_fn_do.ex create mode 100644 impls/elixir.2/lib/steps/step5_tco.ex create mode 100644 impls/elixir.2/lib/steps/step6_file.ex create mode 100644 impls/elixir.2/lib/steps/step7_quote.ex create mode 100644 impls/elixir.2/lib/steps/step8_macros.ex create mode 100644 impls/elixir.2/lib/steps/step9_try.ex create mode 100644 impls/elixir.2/lib/steps/stepA_mal.ex create mode 100644 impls/elixir.2/mix.exs create mode 100644 impls/elixir.2/mix.lock create mode 100755 impls/elixir.2/run diff --git a/IMPLS.yml b/IMPLS.yml index 17e38a53eb..e1ae272096 100644 --- a/IMPLS.yml +++ b/IMPLS.yml @@ -22,6 +22,7 @@ IMPL: - {IMPL: dart} - {IMPL: elisp} - {IMPL: elixir} + - {IMPL: elixir.2} - {IMPL: elm} - {IMPL: erlang, NO_SELF_HOST: 1} # step4 silent exit on "(DO 3)" - {IMPL: es6} diff --git a/Makefile.impls b/Makefile.impls index 2c3517d7ff..c32513d849 100644 --- a/Makefile.impls +++ b/Makefile.impls @@ -33,7 +33,7 @@ wasm_MODE = wasmtime # IMPLS = ada ada.2 awk bash basic bbc-basic c c.2 chuck clojure coffee common-lisp cpp crystal cs d dart \ - elisp elixir elm erlang es6 factor fantom fennel forth fsharp go groovy gnu-smalltalk \ + elisp elixir elixir.2 elm erlang es6 factor fantom fennel forth fsharp go groovy gnu-smalltalk \ guile hare haskell haxe hy io janet java java-truffle js jq julia kotlin latex3 livescript logo lua make mal \ matlab miniMAL nasm nim objc objpascal ocaml perl perl6 php picolisp pike plpgsql \ plsql powershell prolog ps purs python2 python3 r racket rexx rpython ruby ruby.2 rust scala scheme skew sml \ @@ -125,6 +125,7 @@ d_STEP_TO_PROG = impls/d/$($(1)) dart_STEP_TO_PROG = impls/dart/$($(1)).dart elisp_STEP_TO_PROG = impls/elisp/$($(1)).el elixir_STEP_TO_PROG = impls/elixir/lib/mix/tasks/$($(1)).ex +elixir.2_STEP_TO_PROG = impls/elixir.2/lib/steps/$($(1)).ex elm_STEP_TO_PROG = impls/elm/$($(1)).js erlang_STEP_TO_PROG = impls/erlang/$($(1)) es6_STEP_TO_PROG = impls/es6/$($(1)).mjs diff --git a/README.md b/README.md index 3dca01ab2d..7948f3e527 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ process guide](process/guide.md) there is also a [mal/make-a-lisp FAQ](docs/FAQ.md) where I attempt to answer some common questions. -**3. Mal is implemented in 89 languages (95 different implementations and 118 runtime modes)** +**3. Mal is implemented in 89 languages (96 different implementations and 119 runtime modes)** | Language | Creator | | -------- | ------- | @@ -64,6 +64,7 @@ FAQ](docs/FAQ.md) where I attempt to answer some common questions. | [D](#d) | [Dov Murik](https://github.com/dubek) | | [Dart](#dart) | [Harry Terkelsen](https://github.com/hterkelsen) | | [Elixir](#elixir) | [Martin Ek](https://github.com/ekmartin) | +| [Elixir #2](#elixir2) | [Alessandro Cerruti](https://github.com/chrooti) | | [Elm](#elm) | [Jos van Bakel](https://github.com/c0deaddict) | | [Emacs Lisp](#emacs-lisp) | [Vasilij Schneidermann](https://github.com/wasamasa) | | [Erlang](#erlang) | [Nathan Fiedler](https://github.com/nlfiedler) | @@ -445,6 +446,17 @@ mix stepX_YYY iex -S mix stepX_YYY ``` +### Elixir.2 + +The second Elixir implementation of mal has been tested with Elixir 1.19.5 and Erlang/OTP 28. + +``` +cd impls/elixir.2 +STEP=stepX_YYY mix run +# Or with readline/line editing functionality: +STEP=stepX_YYY iex -S mix +``` + ### Elm The Elm implementation of mal has been tested with Elm 0.18.0 diff --git a/impls/elixir.2/.credo.exs b/impls/elixir.2/.credo.exs new file mode 100644 index 0000000000..8d4b68b1e4 --- /dev/null +++ b/impls/elixir.2/.credo.exs @@ -0,0 +1,13 @@ +%{ + configs: [ + %{ + name: "default", + checks: %{ + disabled: [ + {Credo.Check.Readability.ModuleDoc, []}, + {Credo.Check.Refactor.CyclomaticComplexity, []} + ] + } + } + ] +} diff --git a/impls/elixir.2/.formatter.exs b/impls/elixir.2/.formatter.exs new file mode 100644 index 0000000000..d2cda26edd --- /dev/null +++ b/impls/elixir.2/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/impls/elixir.2/.gitignore b/impls/elixir.2/.gitignore new file mode 100644 index 0000000000..48490031ff --- /dev/null +++ b/impls/elixir.2/.gitignore @@ -0,0 +1,25 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Temporary files, for example, from tests. +/tmp/ + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +mal-*.tar + +/.elixir_ls diff --git a/impls/elixir.2/.iex.exs b/impls/elixir.2/.iex.exs new file mode 100644 index 0000000000..8ab89ee50b --- /dev/null +++ b/impls/elixir.2/.iex.exs @@ -0,0 +1 @@ +Mal.start_repl() diff --git a/impls/elixir.2/Dockerfile b/impls/elixir.2/Dockerfile new file mode 100644 index 0000000000..297831898a --- /dev/null +++ b/impls/elixir.2/Dockerfile @@ -0,0 +1,28 @@ +FROM ubuntu:24.04 +MAINTAINER Alessandro Cerruti +LABEL org.opencontainers.image.source=https://github.com/kanaka/mal +LABEL org.opencontainers.image.description="mal test container: Elixir.2" +########################################################## +# General requirements for testing or common across many +# implementations +########################################################## + +RUN apt-get -y update + +# Required for running tests +RUN apt-get -y install make python3 +RUN ln -sf /usr/bin/python3 /usr/bin/python + +# Some typical implementation and test requirements +RUN apt-get -y install curl libreadline-dev libedit-dev + +RUN mkdir -p /mal +WORKDIR /mal + +########################################################## +# Specific implementation requirements +########################################################## + +# Elixir (14) +RUN apt-get install -y erlang-dev elixir +ENV HOME /mal \ No newline at end of file diff --git a/impls/elixir.2/Makefile b/impls/elixir.2/Makefile new file mode 100644 index 0000000000..f575053bce --- /dev/null +++ b/impls/elixir.2/Makefile @@ -0,0 +1,14 @@ +.PHONY: all +all: | deps + mix compile + +deps: + mix local.hex --force + mix deps.get + +lib/steps/*.ex: all + +.PHONY: clean +clean: + mix clean + rm -fr deps _build .elixir_ls \ No newline at end of file diff --git a/impls/elixir.2/lib/mal.ex b/impls/elixir.2/lib/mal.ex new file mode 100644 index 0000000000..9486a35b42 --- /dev/null +++ b/impls/elixir.2/lib/mal.ex @@ -0,0 +1,21 @@ +defmodule Mal do + use Application + + def start(_type, _args) do + if not IEx.started?() do + start_repl() + end + + {:ok, self()} + end + + def start_repl() do + step = + System.fetch_env!("STEP") + |> String.split("_") + |> Enum.map_join(&String.capitalize/1) + + module_name = Module.safe_concat([Mal, step]) + module_name.start() + end +end diff --git a/impls/elixir.2/lib/mal/core.ex b/impls/elixir.2/lib/mal/core.ex new file mode 100644 index 0000000000..d2e72cf48b --- /dev/null +++ b/impls/elixir.2/lib/mal/core.ex @@ -0,0 +1,513 @@ +defmodule Mal.Core do + alias Mal.Env + alias Mal.EvalError + alias Mal.ExceptionWrapperError + alias Mal.Printer + alias Mal.Reader + alias Mal.Type + alias Mal.Type.Function + + @seq_types [:list, :vector] + @collection_types [:list, :vector, :map] + + @spec ns() :: %{String.t() => Type.t()} + def ns do + %{ + "+" => fn + [a, b], env when is_integer(a) and is_integer(b) -> {a + b, env} + args, env -> argserr("+ expects two integers arguments", args, env) + end, + "-" => fn + [a, b], env when is_integer(a) and is_integer(b) -> {a - b, env} + args, env -> argserr("- expects two integers arguments", args, env) + end, + "*" => fn + [a, b], env when is_integer(a) and is_integer(b) -> {a * b, env} + args, env -> argserr("* expects two integers arguments", args, env) + end, + "/" => fn + [a, b], env when is_integer(a) and is_integer(b) -> {Integer.floor_div(a, b), env} + args, env -> argserr("/ expects two integers arguments", args, env) + end, + "pr-str" => fn args, env -> {prn(args, " ", env), env} end, + "str" => fn args, env -> {prn(args, "", env, _print_readably = false), env} end, + "prn" => fn args, env -> + args + |> prn(" ", env) + |> IO.puts() + + {nil, env} + end, + "println" => fn args, env -> + args + |> prn(" ", env, _print_readably = false) + |> IO.puts() + + {nil, env} + end, + "list" => fn args, env -> {make_list(args), env} end, + "list?" => fn + [{:list, _values, _meta}], env -> {true, env} + [_arg], env -> {false, env} + args, env -> argserr("list? expects a single argument", args, env) + end, + "empty?" => fn + [{type, values, _meta}], env when type in @seq_types -> {values == [], env} + args, env -> argserr("empty? expects a single list or vector argument", args, env) + end, + "count" => fn + [{type, values, _meta}], env when type in @seq_types -> {length(values), env} + [nil], env -> {0, env} + args, env -> argserr("count expects a single list, vector or nil argument", args, env) + end, + "=" => fn + [a, b], env -> + {unwrap_vector_recursive(a) == unwrap_vector_recursive(b), env} + + args, env -> + argserr("= expects two arguments", args, env) + end, + "<" => fn + [a, b], env when is_integer(a) and is_integer(b) -> {a < b, env} + args, env -> argserr("< expects two integer arguments", args, env) + end, + "<=" => fn + [a, b], env when is_integer(a) and is_integer(b) -> {a <= b, env} + args, env -> argserr("<= expects two integer arguments", args, env) + end, + ">" => fn + [a, b], env when is_integer(a) and is_integer(b) -> {a > b, env} + args, env -> argserr("> expects two integer arguments", args, env) + end, + ">=" => fn + [a, b], env when is_integer(a) and is_integer(b) -> {a >= b, env} + args, env -> argserr(">= expects two integer arguments", args, env) + end, + "read-string" => fn + [str], env when is_binary(str) -> {Reader.read_str(str), env} + args, env -> argserr("read-string expects a single string argument", args, env) + end, + "slurp" => fn + [filename], env when is_binary(filename) -> {File.read!(filename), env} + args, env -> argserr("slurp expects a single string argument", args, env) + end, + "atom" => fn + [value], env -> + {instance_id, atom_id, env} = Env.make_atom(env, value) + {{:atom, instance_id, atom_id}, env} + + args, env -> + argserr("atom expects a single argument", args, env) + end, + "atom?" => fn + [{:atom, _instance_id, _atom_id}], env -> {true, env} + [_value], env -> {false, env} + args, env -> argserr("atom? expects a single argument", args, env) + end, + "deref" => fn + [{:atom, instance_id, atom_id}], env -> {Env.get_atom(env, instance_id, atom_id), env} + args, env -> argserr("deref expects a single argument", args, env) + end, + "reset!" => fn + [{:atom, instance_id, atom_id}, value], env -> + {value, Env.set_atom(env, instance_id, atom_id, value)} + + args, env -> + argserr("reset! expects two arguments: an atom and a value", args, env) + end, + "swap!" => fn + [{:atom, instance_id, atom_id}, func = %Function{} | args], env -> + old_value = Env.get_atom(env, instance_id, atom_id) + {value, new_env} = func.builtin.([old_value | args], env) + + {value, + Env.set_atom( + %{new_env | curr_instance_id: env.curr_instance_id}, + instance_id, + atom_id, + value + )} + + args, env -> + argserr("swap! expects at least two arguments: an atom and a function", args, env) + end, + "cons" => fn + [value, {type, values, _meta}], env when type in @seq_types -> + {make_list([value | values]), env} + + args, env -> + argserr("cons expects two arguments: a value and a list or a vector", args, env) + end, + "concat" => fn seqs, env -> + result = + seqs + |> Enum.map(fn + {type, values, _meta} when type in @seq_types -> values + arg -> argerr("seqs arguments must be either lists or vectors", arg, env) + end) + |> Enum.concat() + + {make_list(result), env} + end, + "quasiquote" => fn + [ast], env -> {quasiquote(ast), env} + args, env -> argserr("quasiquote expects a single argument", args, env) + end, + "vec" => fn + [{type, values, _meta}], env when type in @seq_types -> {make_vector(values), env} + args, env -> argserr("vec expects a single list or vector argument", args, env) + end, + "nth" => fn + [{type, values, _meta}, idx], env when type in @seq_types and idx >= 0 -> + {nth(values, idx), env} + + args, env -> + argserr("nth expects a single list or vector argument", args, env) + end, + "first" => fn + [{type, values, _meta}], env when type in @seq_types -> + {List.first(values), env} + + [nil], env -> + {nil, env} + + args, env -> + argserr("first expects a single list or vector argument", args, env) + end, + "rest" => fn + [{type, values, _meta}], env when type in @seq_types -> + {values |> rest() |> make_list(), env} + + [nil], env -> + {make_list([]), env} + + args, env -> + argserr("rest expects a single list or vector argument", args, env) + end, + "macro?" => fn + [%Function{} = func], env -> {func.is_macro, env} + [_value], env -> {false, env} + args, env -> argserr("macro? expects a single argument", args, env) + end, + "throw" => &mal_throw/2, + "apply" => fn + [func = %Function{} | args = [_ | _]], env -> + case List.pop_at(args, -1) do + {{type, values, _meta}, args} when type in @seq_types -> + {result, new_env} = func.builtin.(args ++ values, env) + {result, %{new_env | curr_instance_id: env.curr_instance_id}} + + {last_arg, _args} -> + argerr("apply expects a list as last argument", last_arg, env) + end + + args, env -> + argserr("apply expects at least a function and a list argument", args, env) + end, + "map" => fn + [func = %Function{}, {type, values, _meta}], env when type in @seq_types -> + mal_map(func, values, env) + + args, env -> + argserr("map expects a function and a list argument", args, env) + end, + "nil?" => fn + [arg], env -> {arg == nil, env} + args, env -> argserr("nil? expects a single argument", args, env) + end, + "true?" => fn + [arg], env -> {arg == true, env} + args, env -> argserr("true? expects a single argument", args, env) + end, + "false?" => fn + [arg], env -> {arg == false, env} + args, env -> argserr("false? expects a single argument", args, env) + end, + "symbol?" => fn + [{:symbol, _}], env -> {true, env} + [_arg], env -> {false, env} + args, env -> argserr("false? expects a single argument", args, env) + end, + "symbol" => fn + [name], env when is_binary(name) -> {{:symbol, name}, env} + args, env -> argserr("symbol expects a single string argument", args, env) + end, + "keyword" => fn + [name], env when is_binary(name) -> {String.to_atom(name), env} + [name], env when is_atom(name) -> {name, env} + args, env -> argserr("keyword expects a single string argument", args, env) + end, + "keyword?" => fn + [name], env -> {is_atom(name), env} + args, env -> argserr("keyword? expects a single argument", args, env) + end, + "vector" => fn args, env -> {make_vector(args), env} end, + "vector?" => fn + [{:vector, _values, _meta}], env -> {true, env} + [_arg], env -> {false, env} + args, env -> argserr("vector? expects a single argument", args, env) + end, + "sequential?" => fn + [{type, _values, _meta}], env when type in @seq_types -> {true, env} + [_arg], env -> {false, env} + args, env -> argserr("sequential? expects a single argument", args, env) + end, + "hash-map" => fn args, env -> + case merge(%{}, args) do + {:ok, pairs} -> {make_map(pairs), env} + :error -> raise EvalError, message: "hash-map must have an even number of arguments" + end + end, + "map?" => fn + [{:map, _pairs, _meta}], env -> {true, env} + [_arg], env -> {false, env} + args, env -> argserr("map? expects a single argument", args, env) + end, + "assoc" => fn + [{:map, old_pairs, _meta} | args], env -> + case merge(old_pairs, args) do + {:ok, pairs} -> {make_map(pairs), env} + :error -> raise EvalError, message: "assoc must have an even number of arguments" + end + + args, env -> + argserr("assoc expects a map as first argument", args, env) + end, + "dissoc" => fn + [{:map, pairs, _meta} | keys], env -> + {pairs |> Map.drop(keys) |> make_map(), env} + + args, env -> + argserr("dissoc expects a map as first argument", args, env) + end, + "get" => fn + [{:map, pairs, _meta}, key], env -> {Map.get(pairs, key), env} + [nil, _key], env -> {nil, env} + args, env -> argserr("get expects a map or nil and a key argument", args, env) + end, + "contains?" => fn + [{:map, pairs, _meta}, key], env -> {Map.has_key?(pairs, key), env} + args, env -> argserr("contains? expects a map and a key argument", args, env) + end, + "keys" => fn + [{:map, pairs, _meta}], env -> {pairs |> Map.keys() |> make_list(), env} + args, env -> argserr("keys expects a map argument", args, env) + end, + "vals" => fn + [{:map, pairs, _meta}], env -> {pairs |> Map.values() |> make_list(), env} + args, env -> argserr("vals expects a map argument", args, env) + end, + "readline" => fn + [prompt], env when is_binary(prompt) -> + case IO.gets(prompt) do + :eof -> {nil, env} + {:error, reason} -> raise reason + input -> {String.trim_trailing(input, "\n"), env} + end + + args, env -> + argserr("readline expects a string argument", args, env) + end, + "time-ms" => fn + [], env -> {System.os_time(), env} + args, env -> argserr("time-ms expects no argument", args, env) + end, + "meta" => fn + [{type, _values, meta}], env when type in @collection_types -> + {meta, env} + + [%Function{} = func], env -> + {func.meta, env} + + args, env -> + argserr("meta expects a single list, vector, map or function argument", args, env) + end, + "with-meta" => fn + [{type, values, _old_meta}, new_meta], env when type in @collection_types -> + {{type, values, new_meta}, env} + + [%Function{} = func, new_meta], env -> + {%{func | meta: new_meta}, env} + + args, env -> + argserr( + "with-meta expects a list, vector, map or function and a value argument", + args, + env + ) + end, + "fn?" => fn + [%Function{} = func], env -> {not func.is_macro, env} + [_arg], env -> {false, env} + args, env -> argserr("string? expects a single argument", args, env) + end, + "string?" => fn + [string], env -> {is_binary(string), env} + args, env -> argserr("string? expects a single argument", args, env) + end, + "number?" => fn + [integer], env -> {is_integer(integer), env} + args, env -> argserr("number? expects a single argument", args, env) + end, + "seq" => fn + [list = {:list, values, _meta}], env -> + {if(values == [], do: nil, else: list), env} + + [{:vector, values, _meta}], env -> + {if(values == [], do: nil, else: make_list(values)), env} + + [string], env when is_binary(string) -> + {if(string == "", do: nil, else: string |> String.codepoints() |> make_list()), env} + + [nil], env -> + {nil, env} + + args, env -> + argserr("seq expects a single list, vector or string argument", args, env) + end, + "conj" => fn + [{:list, old_values, _meta} | args], env -> + new_list = + args + |> Enum.reduce(old_values, fn arg, values -> [arg | values] end) + |> make_list() + + {new_list, env} + + [{:vector, vector, _meta} | values], env -> + {make_vector(vector ++ values), env} + + args, env -> + argserr("conj takes at least a list or vector argument", args, env) + end + } + |> Map.new(fn + {name, builtin} when is_function(builtin, 2) -> {name, %Function{builtin: builtin}} + end) + end + + @spec argerr(String.t(), Type.t(), Env.t()) :: no_return() + defp argerr(msg, arg, env) do + msg = msg <> ", got " <> Printer.pr_str(arg, env) + + raise EvalError, message: msg + end + + @spec argserr(String.t(), [Type.t()], Env.t()) :: no_return() + defp argserr(msg, args, env) do + msg = msg <> ", got " <> prn(args, ", ", env) + + raise EvalError, message: msg + end + + @spec prn([Type.t()], String.t(), Env.t(), boolean()) :: String.t() + def prn(args, sep, env, print_readably \\ true), + do: Enum.map_join(args, sep, &Printer.pr_str(&1, env, print_readably)) + + @spec unwrap_vector_recursive(Type.vector()) :: [Type.t()] + @spec unwrap_vector_recursive(Type.mal_list()) :: [Type.t()] + @spec unwrap_vector_recursive(Type.mal_map()) :: %{Type.t() => Type.t()} + @spec unwrap_vector_recursive(t) :: t when t: Type.t() + + defp unwrap_vector_recursive({:vector, values, _meta}), + do: Enum.map(values, &unwrap_vector_recursive/1) + + defp unwrap_vector_recursive({:list, values, _meta}), + do: Enum.map(values, &unwrap_vector_recursive/1) + + defp unwrap_vector_recursive({:map, pairs, _meta}), + do: Map.new(pairs, fn {key, value} -> {key, unwrap_vector_recursive(value)} end) + + defp unwrap_vector_recursive(value), do: value + + @spec quasiquote(Type.t()) :: Type.t() + def quasiquote({:vector, values, _meta}), + do: make_list([{:symbol, "vec"}, do_quasiquote(values)]) + + def quasiquote({:list, values, _meta}) do + case values do + [{:symbol, "unquote"}, ast] -> + ast + + [{:symbol, "unquote"}, _ast | _rest] -> + raise EvalError, message: "quasiquote expects a single argument after unquote" + + _ -> + do_quasiquote(values) + end + end + + def quasiquote(map = {:map, _, _}), + do: make_list([{:symbol, "quote"}, map]) + + def quasiquote(symbol = {:symbol, _}), + do: make_list([{:symbol, "quote"}, symbol]) + + def quasiquote(ast), do: ast + + @spec do_quasiquote([Type.t()]) :: Type.mal_list() + defp do_quasiquote([{:list, [{:symbol, "splice-unquote"} | ast], _meta} | rest]) do + case ast do + [ast] -> + make_list([{:symbol, "concat"}, ast, do_quasiquote(rest)]) + + _ -> + raise EvalError, message: "quasiquote expects a single argument after splice-unquote" + end + end + + defp do_quasiquote([ast | rest]) do + make_list([{:symbol, "cons"}, quasiquote(ast), do_quasiquote(rest)]) + end + + defp do_quasiquote([]), do: make_list([]) + + @spec nth([t], integer()) :: t when t: var + defp nth([value | _rest], 0), do: value + defp nth([_value | rest], n), do: nth(rest, n - 1) + + defp nth(_list, _n) do + raise EvalError, message: "nth index out of range" + end + + @spec rest(list()) :: list() + defp rest([_value | rest]), do: rest + defp rest([]), do: [] + + @spec mal_throw([Type.t()], Env.t()) :: no_return() + defp mal_throw([value], env) do + raise ExceptionWrapperError, value: value, env: env + end + + defp mal_throw(args, env) do + argserr("throw expectes a single argument", args, env) + end + + @spec mal_map(Function.t(), [Type.t()], Env.t()) :: {Type.mal_list(), Env.t()} + defp mal_map(func, args, env) do + {values, env} = + Enum.reduce(args, {[], env}, fn value, {args, env} -> + {result, new_env} = func.builtin.([value], env) + {[result | args], %{new_env | curr_instance_id: env.curr_instance_id}} + end) + + {values |> Enum.reverse() |> make_list(), env} + end + + @spec merge(map(), [Type.t()]) :: {:ok, %{Type.t() => Type.t()}} | :error + defp merge(map, [key, value | rest]), + do: merge(Map.put(map, key, value), rest) + + defp merge(map, []), do: {:ok, map} + + defp merge(_map, [_arg]), do: :error + + @spec make_list([Type.t()]) :: Type.mal_list() + def make_list(values), do: {:list, values, nil} + + @spec make_vector([Type.t()]) :: Type.vector() + def make_vector(values), do: {:vector, values, nil} + + @spec make_map(%{Type.t() => Type.t()}) :: Type.mal_map() + def make_map(pairs), do: {:map, pairs, nil} +end diff --git a/impls/elixir.2/lib/mal/empty_statement_error.ex b/impls/elixir.2/lib/mal/empty_statement_error.ex new file mode 100644 index 0000000000..3a3fab990b --- /dev/null +++ b/impls/elixir.2/lib/mal/empty_statement_error.ex @@ -0,0 +1,6 @@ +defmodule Mal.EmptyStatementError do + defexception [] + + @impl true + def message(_), do: "Empty statement" +end diff --git a/impls/elixir.2/lib/mal/env.ex b/impls/elixir.2/lib/mal/env.ex new file mode 100644 index 0000000000..44203942d2 --- /dev/null +++ b/impls/elixir.2/lib/mal/env.ex @@ -0,0 +1,88 @@ +defmodule Mal.Env do + alias Mal.Env.Instance + alias Mal.EvalError + alias Mal.Type + + defstruct instances: %{}, + last_instance_id: 0, + curr_instance_id: 0, + last_atom_id: 0 + + @type t() :: %__MODULE__{ + instances: %{Instance.id() => Instance.t()}, + last_instance_id: Instance.id(), + curr_instance_id: Instance.id(), + last_atom_id: Instance.atom_id() + } + + @spec new() :: t() + def new(), do: new_instance(%__MODULE__{}) + + @spec new_instance(t(), Instance.id() | nil, [Type.t()], [Type.t()]) :: t() + def new_instance(env, outer \\ nil, binds \\ [], exprs \\ []) do + instance = Instance.new(outer, binds, exprs) + + instance_id = env.last_instance_id + 1 + + %{ + env + | instances: Map.put(env.instances, instance_id, instance), + last_instance_id: instance_id, + curr_instance_id: instance_id + } + end + + @spec get!(t(), String.t()) :: Type.t() + def get!(env, symbol) do + case get(env, symbol) do + :error -> + raise EvalError, message: "\'#{symbol}\' not found" + + {:ok, value} -> + value + end + end + + @spec get(t(), String.t()) :: {:ok, Type.t()} | :error + def get(env, symbol), do: do_get(env, env.curr_instance_id, symbol) + + @spec do_get(t(), Instance.id(), String.t()) :: {:ok, Type.t()} | :error + defp do_get(env, id, symbol) do + instance = Map.fetch!(env.instances, id) + + case instance do + %{data: %{^symbol => value}} -> {:ok, value} + %{outer: nil} -> :error + %{outer: outer} -> do_get(env, outer, symbol) + end + end + + @spec set(t(), String.t(), Type.t()) :: t() + def set(env, symbol, value), + do: %{ + env + | instances: + Map.update!(env.instances, env.curr_instance_id, &Instance.set(&1, symbol, value)) + } + + @spec get_atom(t(), Instance.id(), Instance.atom_id()) :: Type.t() + def get_atom(env, instance_id, atom_id) do + %{^instance_id => %{atoms: %{^atom_id => value}}} = env.instances + value + end + + @spec set_atom(t(), Instance.id(), Instance.atom_id(), Type.t()) :: t() + def set_atom(env, instance_id, atom_id, value), + do: %{ + env + | instances: Map.update!(env.instances, instance_id, &Instance.set_atom(&1, atom_id, value)) + } + + @spec make_atom(t(), Type.t()) :: {Instance.id(), Instance.atom_id(), t()} + def make_atom(env, value) do + instance_id = env.curr_instance_id + atom_id = env.last_atom_id + 1 + + {instance_id, atom_id, set_atom(%{env | last_atom_id: atom_id}, instance_id, atom_id, value)} + end +end diff --git a/impls/elixir.2/lib/mal/env/instance.ex b/impls/elixir.2/lib/mal/env/instance.ex new file mode 100644 index 0000000000..d258f4184e --- /dev/null +++ b/impls/elixir.2/lib/mal/env/instance.ex @@ -0,0 +1,54 @@ +defmodule Mal.Env.Instance do + alias Mal.Core + alias Mal.EvalError + alias Mal.Type + + defstruct data: %{}, atoms: %{}, outer: nil + + @type t() :: %__MODULE__{ + data: %{String.t() => Type.t()}, + atoms: %{atom_id() => Type.t()}, + outer: id() | nil + } + + @type id() :: integer() + @type atom_id() :: integer() + + @spec new(id() | nil, [Type.t()], [Type.t()]) :: t() + def new(outer, binds, exprs) do + parse_args(%__MODULE__{outer: outer}, {binds, exprs}, binds, exprs) + end + + @spec parse_args(t(), {[Type.t()], [Type.t()]}, [Type.t()], [Type.t()]) :: t() + defp parse_args(instance, _original_args, [{:symbol, "&"} | binds], exprs) do + case binds do + [{:symbol, bind}] -> + set(instance, bind, Core.make_list(exprs)) + + _ -> + raise EvalError, message: "& must be follwed by a single symbol" + end + end + + defp parse_args(instance, original_args, [{:symbol, bind} | binds], [expr | exprs]) do + instance + |> set(bind, expr) + |> parse_args(original_args, binds, exprs) + end + + defp parse_args(instance, _original_args, [], []), do: instance + + defp parse_args(instance, {binds, exprs}, _binds, _exprs) do + binds_str = Core.prn(binds, ", ", instance.outer) + exprs_str = Core.prn(exprs, ", ", instance.outer) + raise EvalError, message: "Mismatched args: expected #{binds_str} but got #{exprs_str}" + end + + @spec set(t(), String.t(), Type.t()) :: t() + def set(instance, symbol, value), + do: %{instance | data: Map.put(instance.data, symbol, value)} + + @spec set_atom(t(), atom_id(), Type.t()) :: t() + def set_atom(instance, atom_id, value), + do: %{instance | atoms: Map.put(instance.atoms, atom_id, value)} +end diff --git a/impls/elixir.2/lib/mal/eval_error.ex b/impls/elixir.2/lib/mal/eval_error.ex new file mode 100644 index 0000000000..1ca681e597 --- /dev/null +++ b/impls/elixir.2/lib/mal/eval_error.ex @@ -0,0 +1,3 @@ +defmodule Mal.EvalError do + defexception [:message] +end diff --git a/impls/elixir.2/lib/mal/exception_wrapper_error.ex b/impls/elixir.2/lib/mal/exception_wrapper_error.ex new file mode 100644 index 0000000000..7bbec7c80d --- /dev/null +++ b/impls/elixir.2/lib/mal/exception_wrapper_error.ex @@ -0,0 +1,14 @@ +defmodule Mal.ExceptionWrapperError do + alias Mal.Env + alias Mal.Printer + alias Mal.Type + + defexception [:value, :env] + + @type t() :: %__MODULE__{ + value: Type.t(), + env: Env.t() + } + + def message(e), do: Printer.pr_str(e.value, e.env) +end diff --git a/impls/elixir.2/lib/mal/parse_error.ex b/impls/elixir.2/lib/mal/parse_error.ex new file mode 100644 index 0000000000..8887c65473 --- /dev/null +++ b/impls/elixir.2/lib/mal/parse_error.ex @@ -0,0 +1,3 @@ +defmodule Mal.ParseError do + defexception [:message] +end diff --git a/impls/elixir.2/lib/mal/printer.ex b/impls/elixir.2/lib/mal/printer.ex new file mode 100644 index 0000000000..36ff0621e3 --- /dev/null +++ b/impls/elixir.2/lib/mal/printer.ex @@ -0,0 +1,79 @@ +defmodule Mal.Printer do + alias Mal.EvalError + alias Mal.Env + alias Mal.Type.Function + alias Mal.Type + + @spec pr_str(Type.t() | (... -> any()), Env.t() | nil, boolean()) :: String.t() + def pr_str(ast, env \\ nil, print_readably \\ true) + + def pr_str({:list, list, _meta}, env, print_readably), + do: pr_sequence(list, "(", ")", env, print_readably) + + def pr_str(integer, _env, _print_readably) when is_integer(integer), + do: Integer.to_string(integer) + + def pr_str(string, _env, _print_readably = false) when is_binary(string), do: string + + def pr_str(string, _env, _print_readably = true) when is_binary(string), + do: "\"" <> escape_string(string) <> "\"" + + def pr_str(true, _env, _print_readably), do: "true" + def pr_str(false, _env, _print_readably), do: "false" + def pr_str(nil, _env, _print_readably), do: "nil" + def pr_str(atom, _env, _print_readably) when is_atom(atom), do: ":" <> Atom.to_string(atom) + def pr_str({:symbol, symbol}, _env, _print_readably), do: symbol + + def pr_str({:vector, vector, _meta}, env, print_readably), + do: pr_sequence(vector, "[", "]", env, print_readably) + + def pr_str({:atom, instance_id, atom_id}, env = %{}, print_readably), + do: "(atom " <> pr_str(Env.get_atom(env, instance_id, atom_id), env, print_readably) <> ")" + + def pr_str(%Function{}, _env, _print_readably), do: "#" + + def pr_str(func, _env, _print_readably) when is_function(func), do: "#" + + def pr_str({:map, map, _meta}, env, print_readably) do + elements = + Enum.map_join(map, " ", fn {key, value} -> + pr_str(key, env, print_readably) <> " " <> pr_str(value, env, print_readably) + end) + + "{#{elements}}" + end + + def pr_str(arg, _env, _print_readably) do + raise EvalError, message: "Value #{inspect(arg)} unhandled by pr_str" + end + + @spec escape_string(String.t(), String.t()) :: String.t() + def escape_string(string, escaped \\ "") + + def escape_string("", escaped), do: escaped + + def escape_string(<>, escaped), + do: escape_string(rest, <>) + + def escape_string(<>, escaped), + do: escape_string(rest, <>) + + def escape_string(<>, escaped), + do: escape_string(rest, <>) + + def escape_string(<>, escaped), + do: escape_string(rest, <>) + + @spec pr_sequence( + [Type.t()], + String.t(), + String.t(), + Env.t(), + print_readably :: boolean() + ) :: String.t() + defp pr_sequence(seq, opening, closing, env, print_readably) do + elements = Enum.map_join(seq, " ", &pr_str(&1, env, print_readably)) + + opening <> elements <> closing + end +end diff --git a/impls/elixir.2/lib/mal/reader.ex b/impls/elixir.2/lib/mal/reader.ex new file mode 100644 index 0000000000..0eaa1a5964 --- /dev/null +++ b/impls/elixir.2/lib/mal/reader.ex @@ -0,0 +1,187 @@ +defmodule Mal.Reader do + alias Mal.EmptyStatementError + alias Mal.ParseError + alias Mal.Type + + defstruct tokens: [] + + @type t() :: %__MODULE__{ + tokens: [String.t()] + } + + @token_regex ~r/[\s,]*(~@|[\[\]{}()'`~^@]|"(?:\\.|[^\\"])*"?|;.*|[^\s\[\]{}('"`,;)]*)/ + + @spec read_str(String.t()) :: Type.t() + def read_str(str) do + tokens = tokenize(str) + + if tokens == [] do + raise %EmptyStatementError{} + end + + reader = %__MODULE__{tokens: tokens} + {%__MODULE__{tokens: []}, ast} = read_form(reader) + ast + end + + @spec tokenize(String.t()) :: [String.t()] + defp tokenize(str) do + str = String.trim(str) + + Regex.scan(@token_regex, str, capture: :all_but_first) + |> Enum.map(fn [match] -> match end) + # always matches an empty string as the last match + |> List.delete_at(-1) + # remove comments + |> Enum.reject(fn + # comments + <> -> true + # empty match + "" -> true + _ -> false + end) + end + + @spec read_form(t()) :: {t(), Type.t()} + defp read_form(reader) do + case peek(reader) do + "(" -> read_list(next(reader)) + "[" -> read_vector(next(reader)) + "{" -> read_map(next(reader)) + "'" -> read_alias(next(reader), "quote") + "`" -> read_alias(next(reader), "quasiquote") + "~" -> read_alias(next(reader), "unquote") + "~@" -> read_alias(next(reader), "splice-unquote") + "@" -> read_alias(next(reader), "deref") + "^" -> read_alias2(next(reader), "with-meta") + token -> {next(reader), read_atom(token)} + end + end + + @spec read_list(t()) :: {t(), Type.t()} + defp read_list(reader) do + {reader, values} = read_sequence(reader, ")") + {reader, {:list, values, nil}} + end + + @spec read_vector(t()) :: {t(), Type.t()} + defp read_vector(reader) do + {reader, values} = read_sequence(reader, "]") + {reader, {:vector, values, nil}} + end + + @spec read_sequence(t(), binary(), [Type.t()]) :: {t(), [Type.t()]} + defp read_sequence(reader, end_token, seq \\ []) do + case peek(reader) do + ^end_token -> + {next(reader), Enum.reverse(seq)} + + _ -> + {reader, ast} = read_form(reader) + read_sequence(reader, end_token, [ast | seq]) + end + end + + @spec read_map(t(), map()) :: {t(), Type.t()} + defp read_map(reader, pairs \\ %{}) do + case peek(reader) do + "}" -> + {next(reader), {:map, pairs, nil}} + + _ -> + {reader, key} = read_form(reader) + {reader, value} = read_form(reader) + + read_map(reader, Map.put(pairs, key, value)) + end + end + + @spec read_alias(t(), String.t()) :: {t(), Type.t()} + defp read_alias(reader, alias) do + {reader, ast} = read_form(reader) + + {reader, {:list, [{:symbol, alias}, ast], nil}} + end + + @spec read_alias2(t(), String.t()) :: {t(), Type.t()} + defp read_alias2(reader, alias) do + {reader, ast1} = read_form(reader) + {reader, ast2} = read_form(reader) + + {reader, {:list, [{:symbol, alias}, ast2, ast1], nil}} + end + + @spec read_atom(String.t()) :: Type.t() + defp read_atom(token) do + case token do + "false" -> + false + + "true" -> + true + + "nil" -> + nil + + ":" <> keyword -> + String.to_atom(keyword) + + "\"" <> string -> + if byte_size(string) == 0 or :binary.last(string) != ?\" do + raise ParseError, message: "EOF: unterminated string" + end + + string + |> binary_part(0, byte_size(string) - 1) + |> unescape_string() + + symbol_or_integer -> + case Integer.parse(symbol_or_integer) do + {integer, ""} -> integer + :error -> {:symbol, symbol_or_integer} + end + end + end + + @spec unescape_string(String.t(), String.t()) :: String.t() + defp unescape_string(string, unescaped \\ "") + + defp unescape_string("", unescaped), + do: unescaped + + defp unescape_string(<>, unescaped) do + case rest do + <> -> + unescape_string(rest, <>) + + "" -> + raise ParseError, message: "EOF: unterminated escape sequence" + end + end + + defp unescape_string(<>, unescaped), + do: unescape_string(rest, <>) + + @spec unquote_char(?n) :: ?\n + defp unquote_char(?n), do: ?\n + + @spec unquote_char(?\\) :: ?\\ + defp unquote_char(?\\), do: ?\\ + + @spec unquote_char(?") :: ?" + defp unquote_char(?"), do: ?" + + defp unquote_char(_char) do + raise ParseError, message: "EOF: Unexpected escape sequence" + end + + @spec next(t()) :: t() + defp next(reader), do: %{reader | tokens: tl(reader.tokens)} + + @spec peek(t()) :: String.t() + defp peek(%__MODULE__{tokens: [token | _]}), do: token + + defp peek(%__MODULE__{tokens: []}) do + raise ParseError, message: "EOF: unterminated token string" + end +end diff --git a/impls/elixir.2/lib/mal/type.ex b/impls/elixir.2/lib/mal/type.ex new file mode 100644 index 0000000000..13e4f0b4cc --- /dev/null +++ b/impls/elixir.2/lib/mal/type.ex @@ -0,0 +1,26 @@ +defmodule Mal.Type do + alias Mal.Env + alias Mal.Type.Function + + @type t() :: + integer() + | String.t() + | boolean() + | nil + | mal_keyword() + | symbol() + | mal_list() + | vector() + | mal_map() + | Function.t() + | mal_atom() + | ([t()] -> t()) + + @type mal_keyword() :: atom() + @type symbol() :: {:symbol, binary()} + @type mal_list() :: {:list, [t()], t()} + @type vector() :: {:vector, [t()], t()} + @type mal_map() :: {:map, %{t() => t()}, t()} + @type builtin() :: ([t()], Env.t() -> {t(), Env.t()}) + @type mal_atom() :: {:atom, Env.Instance.id(), Env.Instance.atom_id()} +end diff --git a/impls/elixir.2/lib/mal/type/function.ex b/impls/elixir.2/lib/mal/type/function.ex new file mode 100644 index 0000000000..deabfc8a8f --- /dev/null +++ b/impls/elixir.2/lib/mal/type/function.ex @@ -0,0 +1,15 @@ +defmodule Mal.Type.Function do + alias Mal.Env + alias Mal.Type + + defstruct [:body, :argnames, :env_id, :builtin, :meta, is_macro: false] + + @type t() :: %__MODULE__{ + body: Type.t() | nil, + argnames: [Type.symbol()] | nil, + env_id: Env.Instance.id() | nil, + builtin: Type.builtin(), + meta: Type.t(), + is_macro: boolean() + } +end diff --git a/impls/elixir.2/lib/steps/step0_repl.ex b/impls/elixir.2/lib/steps/step0_repl.ex new file mode 100644 index 0000000000..b839e0854f --- /dev/null +++ b/impls/elixir.2/lib/steps/step0_repl.ex @@ -0,0 +1,40 @@ +defmodule Mal.Step0Repl do + @spec start() :: nil + def start, do: rep() + + @spec rep() :: nil + def rep do + case IO.gets("user> ") do + :eof -> + nil + + {:error, reason} -> + raise reason + + data -> + data + |> binary_part(0, byte_size(data) - 1) + |> read() + |> eval() + |> print() + |> IO.puts() + + rep() + end + end + + @spec read(String.t()) :: String.t() + defp read(data) do + data + end + + @spec eval(String.t()) :: String.t() + defp eval(data) do + data + end + + @spec print(String.t()) :: String.t() + defp print(data) do + data + end +end diff --git a/impls/elixir.2/lib/steps/step1_read_print.ex b/impls/elixir.2/lib/steps/step1_read_print.ex new file mode 100644 index 0000000000..4a2528d01f --- /dev/null +++ b/impls/elixir.2/lib/steps/step1_read_print.ex @@ -0,0 +1,53 @@ +defmodule Mal.Step1ReadPrint do + alias Mal.EmptyStatementError + alias Mal.ParseError + alias Mal.Printer + alias Mal.Reader + alias Mal.Type + + @spec start() :: nil + def start, do: rep() + + @spec rep() :: nil + def rep do + case IO.gets("user> ") do + :eof -> + nil + + {:error, reason} -> + raise reason + + input -> + input + |> binary_part(0, byte_size(input) - 1) + |> read() + |> eval() + |> print() + |> IO.puts() + + rep() + end + rescue + e in ParseError -> + IO.puts("** ParseError: #{e.message}") + rep() + + EmptyStatementError -> + rep() + end + + @spec read(String.t()) :: Type.t() + defp read(input) do + Reader.read_str(input) + end + + @spec eval(Type.t()) :: Type.t() + defp eval(ast) do + ast + end + + @spec print(Type.t()) :: String.t() + defp print(ast) do + Printer.pr_str(ast) + end +end diff --git a/impls/elixir.2/lib/steps/step2_eval.ex b/impls/elixir.2/lib/steps/step2_eval.ex new file mode 100644 index 0000000000..9fc34e9f74 --- /dev/null +++ b/impls/elixir.2/lib/steps/step2_eval.ex @@ -0,0 +1,116 @@ +defmodule Mal.Step2Eval do + alias Mal.Core + alias Mal.EmptyStatementError + alias Mal.EvalError + alias Mal.ParseError + alias Mal.Printer + alias Mal.Reader + alias Mal.Type + + @type mal_type() :: Type.t() | (mal_type() -> mal_type()) + + @spec start() :: nil + def start do + rep(%{ + "+" => fn [a, b] -> a + b end, + "-" => fn [a, b] -> a - b end, + "*" => fn [a, b] -> a * b end, + "/" => fn [a, b] -> Integer.floor_div(a, b) end + }) + end + + @spec rep(%{String.t() => mal_type()}) :: nil + def rep(env) do + case IO.gets("user> ") do + :eof -> + nil + + {:error, reason} -> + raise reason + + input -> + input + |> binary_part(0, byte_size(input) - 1) + |> read() + |> eval(env) + |> print() + |> IO.puts() + + rep(env) + end + rescue + e in ParseError -> + IO.puts("** ParseError: #{e.message}") + rep(env) + + EmptyStatementError -> + rep(env) + + e in EvalError -> + IO.puts("** EvalError: #{e.message}") + rep(env) + + e -> + Exception.format(:error, e, __STACKTRACE__) + |> IO.puts() + + rep(env) + end + + @spec read(String.t()) :: Type.t() + defp read(input) do + Reader.read_str(input) + end + + @spec eval(mal_type(), %{String.t() => mal_type()}) :: mal_type() + defp eval(ast, env) do + case Map.fetch(env, "DEBUG-EVAL") do + :error -> + nil + + {:ok, value} when value in [nil, false] -> + nil + + _ -> + IO.puts("EVAL: " <> Printer.pr_str(ast)) + end + + case ast do + {:symbol, symbol} -> + case Map.fetch(env, symbol) do + {:ok, value} -> value + :error -> raise EvalError, message: "#{symbol} not found in env!" + end + + {:list, [name | rest], _meta} -> + func = eval(name, env) + args = Enum.map(rest, &eval(&1, env)) + + case func do + func when is_function(func) -> + func.(args) + + _ -> + raise EvalError, message: "Expected function" + end + + {:vector, values, _meta} -> + values + |> Enum.map(&eval(&1, env)) + |> Core.make_vector() + + {:map, pairs, _meta} -> + pairs + |> Map.new(fn {key, value} -> {key, eval(value, env)} end) + |> Core.make_map() + + ast -> + ast + end + end + + @spec print(mal_type()) :: String.t() + defp print(ast) do + Printer.pr_str(ast) + end +end diff --git a/impls/elixir.2/lib/steps/step3_env.ex b/impls/elixir.2/lib/steps/step3_env.ex new file mode 100644 index 0000000000..90f98c80a1 --- /dev/null +++ b/impls/elixir.2/lib/steps/step3_env.ex @@ -0,0 +1,168 @@ +defmodule Mal.Step3Env do + alias Mal.Core + alias Mal.EmptyStatementError + alias Mal.Env + alias Mal.EvalError + alias Mal.ParseError + alias Mal.Printer + alias Mal.Reader + alias Mal.Type + alias Mal.Type.Function + + @spec start() :: nil + def start() do + env = + Enum.reduce(Core.ns(), Env.new(), fn {key, value}, env -> + Env.set(env, key, value) + end) + + rep(env) + end + + @spec rep(Env.t()) :: nil + def rep(env) do + case IO.gets("user> ") do + :eof -> + nil + + {:error, reason} -> + raise reason + + input -> + {ast, env} = + input + |> binary_part(0, byte_size(input) - 1) + |> read() + |> eval(env) + + ast + |> print() + |> IO.puts() + + rep(env) + end + rescue + e in ParseError -> + IO.puts("** ParseError: #{e.message}") + rep(env) + + EmptyStatementError -> + rep(env) + + e in EvalError -> + IO.puts("** EvalError: #{e.message}") + rep(env) + + e -> + Exception.format(:error, e, __STACKTRACE__) + |> IO.puts() + + rep(env) + end + + @spec read(String.t()) :: Type.t() + defp read(input) do + Reader.read_str(input) + end + + @spec eval(Type.t(), Env.t()) :: {Type.t(), Env.t()} + defp eval(ast, env) do + case Env.get(env, "DEBUG-EVAL") do + :error -> + nil + + {:ok, value} when value in [nil, false] -> + nil + + _ -> + IO.puts("EVAL: " <> Printer.pr_str(ast)) + end + + case ast do + {:symbol, symbol} -> + {Env.get!(env, symbol), env} + + {:list, values = [_ | _], _meta} -> + eval_list(values, env) + + {:vector, values, _meta} -> + {values, env} = + Enum.reduce(values, {[], env}, fn value, {values, env} -> + {result, env} = eval(value, env) + {[result | values], env} + end) + + {values |> Enum.reverse() |> Core.make_vector(), env} + + {:map, pairs, _meta} -> + {pairs, env} = + Enum.reduce(pairs, {%{}, env}, fn {key, value}, {pairs, env} -> + {result, env} = eval(value, env) + {Map.put(pairs, key, result), env} + end) + + {Core.make_map(pairs), env} + + ast -> + {ast, env} + end + end + + @spec eval_list(nonempty_list(Type.t()), Env.t()) :: {Type.t(), Env.t()} + defp eval_list([{:symbol, "def!"} | rest], env) do + [{:symbol, key}, value] = rest + + {value, env} = eval(value, env) + {value, Env.set(env, key, value)} + end + + defp eval_list([{:symbol, "let*"} | rest], env) do + [bindings, exprs] = rest + + inner_env = + env + |> Env.new_instance(env.curr_instance_id) + |> parse_let_bindings(unwrap_seq(bindings)) + + {result, inner_env} = eval(exprs, inner_env) + {result, %{inner_env | curr_instance_id: env.curr_instance_id}} + end + + defp eval_list(values, env) do + {values, env} = + Enum.reduce(values, {[], env}, fn value, {values, env} -> + {value, env} = eval(value, env) + {[value | values], env} + end) + + [func | args] = Enum.reverse(values) + + case func do + %Function{} -> + func.builtin.(args, env) + + _ -> + raise EvalError, + message: "Expected function, got " <> Printer.pr_str(func, env) + end + end + + @spec unwrap_seq(Type.mal_list() | Type.vector()) :: [Type.t()] + defp unwrap_seq({type, values, _meta}) when type in [:list, :vector], do: values + + @spec parse_let_bindings(Env.t(), [Type.t()]) :: Env.t() + defp parse_let_bindings(env, []), do: env + + defp parse_let_bindings(env, [{:symbol, key}, value | rest]) do + {value, new_env} = eval(value, env) + + %{new_env | curr_instance_id: env.curr_instance_id} + |> Env.set(key, value) + |> parse_let_bindings(rest) + end + + @spec print(Type.t()) :: String.t() + defp print(ast) do + Printer.pr_str(ast) + end +end diff --git a/impls/elixir.2/lib/steps/step4_if_fn_do.ex b/impls/elixir.2/lib/steps/step4_if_fn_do.ex new file mode 100644 index 0000000000..64e4d7ba59 --- /dev/null +++ b/impls/elixir.2/lib/steps/step4_if_fn_do.ex @@ -0,0 +1,211 @@ +defmodule Mal.Step4IfFnDo do + alias Mal.Core + alias Mal.EmptyStatementError + alias Mal.Env + alias Mal.EvalError + alias Mal.ParseError + alias Mal.Printer + alias Mal.Reader + alias Mal.Type + alias Mal.Type.Function + + @spec start() :: nil + def start() do + env = + Enum.reduce(Core.ns(), Env.new(), fn {key, value}, env -> + Env.set(env, key, value) + end) + + {_, env} = rep("(def! not (fn* (a) (if a false true)))", env) + + loop(env) + end + + @spec loop(Env.t()) :: nil + defp loop(env) do + case IO.gets("user> ") do + :eof -> + nil + + {:error, reason} -> + raise reason + + input -> + {str, env} = + input + |> binary_part(0, byte_size(input) - 1) + |> rep(env) + + IO.puts(str) + + loop(env) + end + rescue + e in ParseError -> + IO.puts("** ParseError: #{e.message}") + loop(env) + + EmptyStatementError -> + loop(env) + + e in EvalError -> + IO.puts("** EvalError: #{e.message}") + loop(env) + + e -> + Exception.format(:error, e, __STACKTRACE__) + |> IO.puts() + + loop(env) + end + + @spec rep(String.t(), Env.t()) :: {String.t(), Env.t()} + defp rep(input, env) do + {ast, env} = + input + |> read() + |> eval(env) + + {print(ast), env} + end + + @spec read(String.t()) :: Type.t() + defp read(input) do + Reader.read_str(input) + end + + @spec eval(Type.t(), Env.t()) :: {Type.t(), Env.t()} + defp eval(ast, env) do + case Env.get(env, "DEBUG-EVAL") do + :error -> + nil + + {:ok, value} when value in [nil, false] -> + nil + + _ -> + IO.puts("EVAL: " <> Printer.pr_str(ast)) + end + + case ast do + {:symbol, symbol} -> + {Env.get!(env, symbol), env} + + {:list, values = [_ | _], _meta} -> + eval_list(values, env) + + {:vector, values, _meta} -> + {values, env} = + Enum.reduce(values, {[], env}, fn value, {values, env} -> + {result, env} = eval(value, env) + {[result | values], env} + end) + + {values |> Enum.reverse() |> Core.make_vector(), env} + + {:map, pairs, _meta} -> + {pairs, env} = + Enum.reduce(pairs, {%{}, env}, fn {key, value}, {pairs, env} -> + {result, env} = eval(value, env) + {Map.put(pairs, key, result), env} + end) + + {Core.make_map(pairs), env} + + ast -> + {ast, env} + end + end + + @spec eval_list(nonempty_list(Type.t()), Env.t()) :: {Type.t(), Env.t()} + defp eval_list([{:symbol, "def!"} | rest], env) do + [{:symbol, key}, value] = rest + + {value, env} = eval(value, env) + {value, Env.set(env, key, value)} + end + + defp eval_list([{:symbol, "let*"} | rest], env) do + [bindings, exprs] = rest + + inner_env = + env + |> Env.new_instance(env.curr_instance_id) + |> parse_let_bindings(unwrap_seq(bindings)) + + {result, inner_env} = eval(exprs, inner_env) + {result, %{inner_env | curr_instance_id: env.curr_instance_id}} + end + + defp eval_list([{:symbol, "do"} | rest], env) do + Enum.reduce(rest, {nil, env}, fn ast, {_result, env} -> eval(ast, env) end) + end + + defp eval_list([{:symbol, "if"} | rest], env) do + [condition, if_true | rest] = rest + + {result, env} = eval(condition, env) + + if result in [nil, false] do + case rest do + [if_false] -> eval(if_false, env) + [] -> {nil, env} + end + else + eval(if_true, env) + end + end + + defp eval_list([{:symbol, "fn*"} | rest], env) do + [argnames, body] = rest + + func = fn args, call_env -> + inner_env = + Env.new_instance(call_env, env.curr_instance_id, unwrap_seq(argnames), args) + + {result, inner_env} = eval(body, inner_env) + + {result, %{inner_env | curr_instance_id: call_env.curr_instance_id}} + end + + {%Function{builtin: func}, env} + end + + defp eval_list(values, env) do + {values, env} = + Enum.reduce(values, {[], env}, fn value, {values, env} -> + {value, env} = eval(value, env) + {[value | values], env} + end) + + [func | args] = Enum.reverse(values) + + case func do + %Function{} -> + func.builtin.(args, env) + + _ -> + raise EvalError, + message: "Expected function, got " <> Printer.pr_str(func, env) + end + end + + @spec unwrap_seq(Type.mal_list() | Type.vector()) :: [Type.t()] + defp unwrap_seq({type, values, _meta}) when type in [:list, :vector], do: values + + @spec parse_let_bindings(Env.t(), [Type.t()]) :: Env.t() + defp parse_let_bindings(env, []), do: env + + defp parse_let_bindings(env, [{:symbol, key}, value | rest]) do + {value, new_env} = eval(value, env) + + %{new_env | curr_instance_id: env.curr_instance_id} + |> Env.set(key, value) + |> parse_let_bindings(rest) + end + + @spec print(Type.t()) :: String.t() + defp print(ast) do + Printer.pr_str(ast) + end +end diff --git a/impls/elixir.2/lib/steps/step5_tco.ex b/impls/elixir.2/lib/steps/step5_tco.ex new file mode 100644 index 0000000000..39e52bc6c9 --- /dev/null +++ b/impls/elixir.2/lib/steps/step5_tco.ex @@ -0,0 +1,238 @@ +defmodule Mal.Step5Tco do + alias Mal.Core + alias Mal.EmptyStatementError + alias Mal.Env + alias Mal.EvalError + alias Mal.ParseError + alias Mal.Printer + alias Mal.Reader + alias Mal.Type + alias Mal.Type.Function + + @spec start() :: nil + def start() do + env = + Enum.reduce(Core.ns(), Env.new(), fn {key, value}, env -> + Env.set(env, key, value) + end) + + {_, env} = rep("(def! not (fn* (a) (if a false true)))", env) + + loop(env) + end + + @spec loop(Env.t()) :: nil + defp loop(env) do + case IO.gets("user> ") do + :eof -> + nil + + {:error, reason} -> + raise reason + + input -> + {str, env} = + input + |> binary_part(0, byte_size(input) - 1) + |> rep(env) + + IO.puts(str) + + loop(env) + end + rescue + e in ParseError -> + IO.puts("** ParseError: #{e.message}") + loop(env) + + EmptyStatementError -> + loop(env) + + e in EvalError -> + IO.puts("** EvalError: #{e.message}") + loop(env) + + e -> + Exception.format(:error, e, __STACKTRACE__) + |> IO.puts() + + loop(env) + end + + @spec rep(String.t(), Env.t()) :: {String.t(), Env.t()} + defp rep(input, env) do + {ast, new_env} = + input + |> read() + |> eval(env) + + {print(ast), %{new_env | curr_instance_id: env.curr_instance_id}} + end + + @spec read(String.t()) :: Type.t() + defp read(input) do + Reader.read_str(input) + end + + @spec eval(Type.t(), Env.t()) :: {Type.t(), Env.t()} + defp eval(ast, env) do + case Env.get(env, "DEBUG-EVAL") do + :error -> + nil + + {:ok, value} when value in [nil, false] -> + nil + + _ -> + IO.puts("EVAL: " <> Printer.pr_str(ast)) + end + + case ast do + {:symbol, symbol} -> + {Env.get!(env, symbol), env} + + {:list, values = [_ | _], _meta} -> + eval_list(values, env) + + {:vector, values, _meta} -> + {values, env} = + Enum.reduce(values, {[], env}, fn value, {values, env} -> + {result, new_env} = eval(value, env) + {[result | values], %{new_env | curr_instance_id: env.curr_instance_id}} + end) + + {values |> Enum.reverse() |> Core.make_vector(), env} + + {:map, pairs, _meta} -> + {pairs, env} = + Enum.reduce(pairs, {%{}, env}, fn {key, value}, {pairs, env} -> + {result, new_env} = eval(value, env) + {Map.put(pairs, key, result), %{new_env | curr_instance_id: env.curr_instance_id}} + end) + + {Core.make_map(pairs), env} + + ast -> + {ast, env} + end + end + + @spec eval_list(nonempty_list(Type.t()), Env.t()) :: {Type.t(), Env.t()} + defp eval_list([{:symbol, "def!"} | rest], env) do + [{:symbol, key}, value] = rest + + curr_instance_id = env.curr_instance_id + {value, env} = eval(value, env) + {value, Env.set(%{env | curr_instance_id: curr_instance_id}, key, value)} + end + + defp eval_list([{:symbol, "let*"} | rest], env) do + [bindings, exprs] = rest + + inner_env = + env + |> Env.new_instance(env.curr_instance_id) + |> parse_let_bindings(unwrap_seq(bindings)) + + eval(exprs, inner_env) + end + + defp eval_list([{:symbol, "do"} | rest], env) do + eval_do(rest, env) + end + + defp eval_list([{:symbol, "if"} | rest], env) do + [condition, if_true | rest] = rest + + {result, new_env} = eval(condition, env) + env = %{new_env | curr_instance_id: env.curr_instance_id} + + if result in [nil, false] do + case rest do + [if_false] -> eval(if_false, env) + [] -> {nil, env} + end + else + eval(if_true, env) + end + end + + defp eval_list([{:symbol, "fn*"} | rest], env) do + [argnames, body] = rest + + func = %Function{ + body: body, + argnames: argnames, + env_id: env.curr_instance_id, + builtin: fn args, call_env -> + call(call_env, env.curr_instance_id, argnames, args, body) + end + } + + {func, env} + end + + defp eval_list(values, env) do + {values, env} = + Enum.reduce(values, {[], env}, fn value, {values, env} -> + {value, new_env} = eval(value, env) + {[value | values], %{new_env | curr_instance_id: env.curr_instance_id}} + end) + + [func | args] = Enum.reverse(values) + + case func do + %Function{body: nil} -> + func.builtin.(args, env) + + %Function{} -> + call(env, func.env_id, func.argnames, args, func.body) + + _ -> + raise EvalError, + message: "Expected function, got " <> Printer.pr_str(func, env) + end + end + + @spec unwrap_seq(Type.mal_list() | Type.vector()) :: [Type.t()] + defp unwrap_seq({type, values, _meta}) when type in [:list, :vector], do: values + + @spec parse_let_bindings(Env.t(), [Type.t()]) :: Env.t() + defp parse_let_bindings(env, []), do: env + + defp parse_let_bindings(env, [{:symbol, key}, value | rest]) do + {value, new_env} = eval(value, env) + + %{new_env | curr_instance_id: env.curr_instance_id} + |> Env.set(key, value) + |> parse_let_bindings(rest) + end + + @spec eval_do([Type.t()], Env.t()) :: {Type.t(), Env.t()} + defp eval_do([], env), do: {nil, env} + + defp eval_do([ast], env), do: eval(ast, env) + + defp eval_do([ast | rest], env) do + {_result, new_env} = eval(ast, env) + eval_do(rest, %{new_env | curr_instance_id: env.curr_instance_id}) + end + + @spec call( + env :: Env.t(), + outer_env_id :: Env.Instance.id(), + argnames :: Type.t(), + args :: [Type.t()], + body :: Type.t() + ) :: {Type.t(), Env.t()} + defp call(env, outer_env_id, argnames, args, body) do + inner_env = Env.new_instance(env, outer_env_id, unwrap_seq(argnames), args) + + eval(body, inner_env) + end + + @spec print(Type.t()) :: String.t() + defp print(ast) do + Printer.pr_str(ast) + end +end diff --git a/impls/elixir.2/lib/steps/step6_file.ex b/impls/elixir.2/lib/steps/step6_file.ex new file mode 100644 index 0000000000..3198faeaca --- /dev/null +++ b/impls/elixir.2/lib/steps/step6_file.ex @@ -0,0 +1,259 @@ +defmodule Mal.Step6File do + alias Mal.Core + alias Mal.EmptyStatementError + alias Mal.Env + alias Mal.EvalError + alias Mal.Type.Function + alias Mal.ParseError + alias Mal.Printer + alias Mal.Reader + alias Mal.Type + + @spec start() :: nil + def start() do + env = + Enum.reduce(Core.ns(), Env.new(), fn {key, value}, env -> + Env.set(env, key, value) + end) + + eval_builtin = fn [ast], call_env -> + eval(ast, %{call_env | curr_instance_id: env.curr_instance_id}) + end + + env = Env.set(env, "eval", %Function{builtin: eval_builtin}) + + {_, env} = rep("(def! not (fn* (a) (if a false true)))", env) + + {_, env} = + rep( + "(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \"\nnil)\")))))", + env + ) + + case System.argv() do + [filename | args] -> + _ = rep("(load-file \"#{filename}\")", Env.set(env, "*ARGV*", Core.make_list(args))) + nil + + [] -> + env + |> Env.set("*ARGV*", Core.make_list([])) + |> loop() + end + end + + @spec loop(Env.t()) :: nil + defp loop(env) do + case IO.gets("user> ") do + :eof -> + nil + + {:error, reason} -> + raise reason + + input -> + {str, env} = + input + |> binary_part(0, byte_size(input) - 1) + |> rep(env) + + IO.puts(str) + + loop(env) + end + rescue + e in ParseError -> + IO.puts("** ParseError: #{e.message}") + loop(env) + + EmptyStatementError -> + loop(env) + + e in EvalError -> + IO.puts("** EvalError: #{e.message}") + loop(env) + + e -> + Exception.format(:error, e, __STACKTRACE__) + |> IO.puts() + + loop(env) + end + + @spec rep(String.t(), Env.t()) :: {String.t(), Env.t()} + defp rep(input, env) do + {ast, new_env} = + input + |> read() + |> eval(env) + + {print(ast, new_env), %{new_env | curr_instance_id: env.curr_instance_id}} + end + + @spec read(String.t()) :: Type.t() + defp read(input) do + Reader.read_str(input) + end + + @spec eval(Type.t(), Env.t()) :: {Type.t(), Env.t()} + defp eval(ast, env) do + case Env.get(env, "DEBUG-EVAL") do + :error -> + nil + + {:ok, value} when value in [nil, false] -> + nil + + _ -> + IO.puts("EVAL: " <> Printer.pr_str(ast)) + end + + case ast do + {:symbol, symbol} -> + {Env.get!(env, symbol), env} + + {:list, values = [_ | _], _meta} -> + eval_list(values, env) + + {:vector, values, _meta} -> + {values, env} = + Enum.reduce(values, {[], env}, fn value, {values, env} -> + {result, new_env} = eval(value, env) + {[result | values], %{new_env | curr_instance_id: env.curr_instance_id}} + end) + + {values |> Enum.reverse() |> Core.make_vector(), env} + + {:map, pairs, _meta} -> + {pairs, env} = + Enum.reduce(pairs, {%{}, env}, fn {key, value}, {pairs, env} -> + {result, new_env} = eval(value, env) + {Map.put(pairs, key, result), %{new_env | curr_instance_id: env.curr_instance_id}} + end) + + {Core.make_map(pairs), env} + + ast -> + {ast, env} + end + end + + @spec eval_list(nonempty_list(Type.t()), Env.t()) :: {Type.t(), Env.t()} + defp eval_list([{:symbol, "def!"} | rest], env) do + [{:symbol, key}, value] = rest + + curr_instance_id = env.curr_instance_id + {value, env} = eval(value, env) + {value, Env.set(%{env | curr_instance_id: curr_instance_id}, key, value)} + end + + defp eval_list([{:symbol, "let*"} | rest], env) do + [bindings, exprs] = rest + + inner_env = + env + |> Env.new_instance(env.curr_instance_id) + |> parse_let_bindings(unwrap_seq(bindings)) + + eval(exprs, inner_env) + end + + defp eval_list([{:symbol, "do"} | rest], env) do + eval_do(rest, env) + end + + defp eval_list([{:symbol, "if"} | rest], env) do + [condition, if_true | rest] = rest + + {result, new_env} = eval(condition, env) + env = %{new_env | curr_instance_id: env.curr_instance_id} + + if result in [nil, false] do + case rest do + [if_false] -> eval(if_false, env) + [] -> {nil, env} + end + else + eval(if_true, env) + end + end + + defp eval_list([{:symbol, "fn*"} | rest], env) do + [argnames, body] = rest + + func = %Function{ + body: body, + argnames: argnames, + env_id: env.curr_instance_id, + builtin: fn args, call_env -> + call(call_env, env.curr_instance_id, argnames, args, body) + end + } + + {func, env} + end + + defp eval_list(values, env) do + {values, env} = + Enum.reduce(values, {[], env}, fn value, {values, env} -> + {value, new_env} = eval(value, env) + {[value | values], %{new_env | curr_instance_id: env.curr_instance_id}} + end) + + [func | args] = Enum.reverse(values) + + case func do + %Function{body: nil} -> + func.builtin.(args, env) + + %Function{} -> + call(env, func.env_id, func.argnames, args, func.body) + + _ -> + raise EvalError, + message: "Expected function, got " <> Printer.pr_str(func, env) + end + end + + @spec unwrap_seq(Type.mal_list() | Type.vector()) :: [Type.t()] + defp unwrap_seq({type, values, _meta}) when type in [:list, :vector], do: values + + @spec parse_let_bindings(Env.t(), [Type.t()]) :: Env.t() + defp parse_let_bindings(env, []), do: env + + defp parse_let_bindings(env, [{:symbol, key}, value | rest]) do + {value, new_env} = eval(value, env) + + %{new_env | curr_instance_id: env.curr_instance_id} + |> Env.set(key, value) + |> parse_let_bindings(rest) + end + + @spec eval_do([Type.t()], Env.t()) :: {Type.t(), Env.t()} + defp eval_do([], env), do: {nil, env} + + defp eval_do([ast], env), do: eval(ast, env) + + defp eval_do([ast | rest], env) do + {_result, new_env} = eval(ast, env) + eval_do(rest, %{new_env | curr_instance_id: env.curr_instance_id}) + end + + @spec call( + env :: Env.t(), + outer_env_id :: Env.Instance.id(), + argnames :: Type.t(), + args :: [Type.t()], + body :: Type.t() + ) :: {Type.t(), Env.t()} + defp call(env, outer_env_id, argnames, args, body) do + inner_env = Env.new_instance(env, outer_env_id, unwrap_seq(argnames), args) + + eval(body, inner_env) + end + + @spec print(Type.t(), Env.t()) :: String.t() + defp print(ast, env) do + Printer.pr_str(ast, env) + end +end diff --git a/impls/elixir.2/lib/steps/step7_quote.ex b/impls/elixir.2/lib/steps/step7_quote.ex new file mode 100644 index 0000000000..4b7cd3e214 --- /dev/null +++ b/impls/elixir.2/lib/steps/step7_quote.ex @@ -0,0 +1,269 @@ +defmodule Mal.Step7Quote do + alias Mal.Core + alias Mal.EmptyStatementError + alias Mal.Env + alias Mal.EvalError + alias Mal.Type.Function + alias Mal.ParseError + alias Mal.Printer + alias Mal.Reader + alias Mal.Type + + @spec start() :: nil + def start() do + env = + Enum.reduce(Core.ns(), Env.new(), fn {key, value}, env -> + Env.set(env, key, value) + end) + + eval_builtin = fn [ast], call_env -> + eval(ast, %{call_env | curr_instance_id: env.curr_instance_id}) + end + + env = Env.set(env, "eval", %Function{builtin: eval_builtin}) + + {_, env} = rep("(def! not (fn* (a) (if a false true)))", env) + + {_, env} = + rep( + "(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \"\nnil)\")))))", + env + ) + + case System.argv() do + [filename | args] -> + _ = rep("(load-file \"#{filename}\")", Env.set(env, "*ARGV*", Core.make_list(args))) + nil + + [] -> + env + |> Env.set("*ARGV*", Core.make_list([])) + |> loop() + end + end + + @spec loop(Env.t()) :: nil + defp loop(env) do + case IO.gets("user> ") do + :eof -> + nil + + {:error, reason} -> + raise reason + + input -> + {str, env} = + input + |> binary_part(0, byte_size(input) - 1) + |> rep(env) + + IO.puts(str) + + loop(env) + end + rescue + e in ParseError -> + IO.puts("** ParseError: #{e.message}") + loop(env) + + EmptyStatementError -> + loop(env) + + e in EvalError -> + IO.puts("** EvalError: #{e.message}") + loop(env) + + e -> + Exception.format(:error, e, __STACKTRACE__) + |> IO.puts() + + loop(env) + end + + @spec rep(String.t(), Env.t()) :: {String.t(), Env.t()} + defp rep(input, env) do + {ast, new_env} = + input + |> read() + |> eval(env) + + {print(ast, new_env), %{new_env | curr_instance_id: env.curr_instance_id}} + end + + @spec read(String.t()) :: Type.t() + defp read(input) do + Reader.read_str(input) + end + + @spec eval(Type.t(), Env.t()) :: {Type.t(), Env.t()} + defp eval(ast, env) do + case Env.get(env, "DEBUG-EVAL") do + :error -> + nil + + {:ok, value} when value in [nil, false] -> + nil + + _ -> + IO.puts("EVAL: " <> Printer.pr_str(ast)) + end + + case ast do + {:symbol, symbol} -> + {Env.get!(env, symbol), env} + + {:list, values = [_ | _], _meta} -> + eval_list(values, env) + + {:vector, values, _meta} -> + {values, env} = + Enum.reduce(values, {[], env}, fn value, {values, env} -> + {result, new_env} = eval(value, env) + {[result | values], %{new_env | curr_instance_id: env.curr_instance_id}} + end) + + {values |> Enum.reverse() |> Core.make_vector(), env} + + {:map, pairs, _meta} -> + {pairs, env} = + Enum.reduce(pairs, {%{}, env}, fn {key, value}, {pairs, env} -> + {result, new_env} = eval(value, env) + {Map.put(pairs, key, result), %{new_env | curr_instance_id: env.curr_instance_id}} + end) + + {Core.make_map(pairs), env} + + ast -> + {ast, env} + end + end + + @spec eval_list(nonempty_list(Type.t()), Env.t()) :: {Type.t(), Env.t()} + defp eval_list([{:symbol, "def!"} | rest], env) do + [{:symbol, key}, value] = rest + + curr_instance_id = env.curr_instance_id + {value, env} = eval(value, env) + {value, Env.set(%{env | curr_instance_id: curr_instance_id}, key, value)} + end + + defp eval_list([{:symbol, "let*"} | rest], env) do + [bindings, exprs] = rest + + inner_env = + env + |> Env.new_instance(env.curr_instance_id) + |> parse_let_bindings(unwrap_seq(bindings)) + + eval(exprs, inner_env) + end + + defp eval_list([{:symbol, "do"} | rest], env) do + eval_do(rest, env) + end + + defp eval_list([{:symbol, "if"} | rest], env) do + [condition, if_true | rest] = rest + + {result, new_env} = eval(condition, env) + env = %{new_env | curr_instance_id: env.curr_instance_id} + + if result in [nil, false] do + case rest do + [if_false] -> eval(if_false, env) + [] -> {nil, env} + end + else + eval(if_true, env) + end + end + + defp eval_list([{:symbol, "fn*"} | rest], env) do + [argnames, body] = rest + + func = %Function{ + body: body, + argnames: argnames, + env_id: env.curr_instance_id, + builtin: fn args, call_env -> + call(call_env, env.curr_instance_id, argnames, args, body) + end + } + + {func, env} + end + + defp eval_list([{:symbol, "quote"} | rest], env) do + [arg] = rest + {arg, env} + end + + defp eval_list([{:symbol, "quasiquote"} | rest], env) do + [arg] = rest + eval(Core.quasiquote(arg), env) + end + + defp eval_list(values, env) do + {values, env} = + Enum.reduce(values, {[], env}, fn value, {values, env} -> + {value, new_env} = eval(value, env) + {[value | values], %{new_env | curr_instance_id: env.curr_instance_id}} + end) + + [func | args] = Enum.reverse(values) + + case func do + %Function{body: nil} -> + func.builtin.(args, env) + + %Function{} -> + call(env, func.env_id, func.argnames, args, func.body) + + _ -> + raise EvalError, + message: "Expected function, got " <> Printer.pr_str(func, env) + end + end + + @spec unwrap_seq(Type.mal_list() | Type.vector()) :: [Type.t()] + defp unwrap_seq({type, values, _meta}) when type in [:list, :vector], do: values + + @spec parse_let_bindings(Env.t(), [Type.t()]) :: Env.t() + defp parse_let_bindings(env, []), do: env + + defp parse_let_bindings(env, [{:symbol, key}, value | rest]) do + {value, new_env} = eval(value, env) + + %{new_env | curr_instance_id: env.curr_instance_id} + |> Env.set(key, value) + |> parse_let_bindings(rest) + end + + @spec eval_do([Type.t()], Env.t()) :: {Type.t(), Env.t()} + defp eval_do([], env), do: {nil, env} + + defp eval_do([ast], env), do: eval(ast, env) + + defp eval_do([ast | rest], env) do + {_result, new_env} = eval(ast, env) + eval_do(rest, %{new_env | curr_instance_id: env.curr_instance_id}) + end + + @spec call( + env :: Env.t(), + outer_env_id :: Env.Instance.id(), + argnames :: Type.t(), + args :: [Type.t()], + body :: Type.t() + ) :: {Type.t(), Env.t()} + defp call(env, outer_env_id, argnames, args, body) do + inner_env = Env.new_instance(env, outer_env_id, unwrap_seq(argnames), args) + + eval(body, inner_env) + end + + @spec print(Type.t(), Env.t()) :: String.t() + defp print(ast, env) do + Printer.pr_str(ast, env) + end +end diff --git a/impls/elixir.2/lib/steps/step8_macros.ex b/impls/elixir.2/lib/steps/step8_macros.ex new file mode 100644 index 0000000000..3a05933df3 --- /dev/null +++ b/impls/elixir.2/lib/steps/step8_macros.ex @@ -0,0 +1,297 @@ +defmodule Mal.Step8Macros do + alias Mal.Core + alias Mal.EmptyStatementError + alias Mal.Env + alias Mal.EvalError + alias Mal.Type.Function + alias Mal.ParseError + alias Mal.Printer + alias Mal.Reader + alias Mal.Type + + @spec start() :: nil + def start() do + env = + Enum.reduce(Core.ns(), Env.new(), fn {key, value}, env -> + Env.set(env, key, value) + end) + + eval_builtin = fn [ast], call_env -> + eval(ast, %{call_env | curr_instance_id: env.curr_instance_id}) + end + + env = Env.set(env, "eval", %Function{builtin: eval_builtin}) + + {_, env} = rep("(def! not (fn* (a) (if a false true)))", env) + + {_, env} = + rep( + "(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \"\nnil)\")))))", + env + ) + + {_, env} = + rep( + "(defmacro! cond (fn* (& xs) (if (> (count xs) 0) (list 'if (first xs) (if (> (count xs) 1) (nth xs 1) (throw \"odd number of forms to cond\")) (cons 'cond (rest (rest xs)))))))", + env + ) + + case System.argv() do + [filename | args] -> + _ = rep("(load-file \"#{filename}\")", Env.set(env, "*ARGV*", Core.make_list(args))) + nil + + [] -> + env + |> Env.set("*ARGV*", Core.make_list([])) + |> loop() + end + end + + @spec loop(Env.t()) :: nil + defp loop(env) do + case IO.gets("user> ") do + :eof -> + nil + + {:error, reason} -> + raise reason + + input -> + {str, env} = + input + |> binary_part(0, byte_size(input) - 1) + |> rep(env) + + IO.puts(str) + + loop(env) + end + rescue + e in ParseError -> + IO.puts("** ParseError: #{e.message}") + loop(env) + + EmptyStatementError -> + loop(env) + + e in EvalError -> + IO.puts("** EvalError: #{e.message}") + loop(env) + + e -> + Exception.format(:error, e, __STACKTRACE__) + |> IO.puts() + + loop(env) + end + + @spec rep(String.t(), Env.t()) :: {String.t(), Env.t()} + defp rep(input, env) do + {ast, new_env} = + input + |> read() + |> eval(env) + + {print(ast, new_env), %{new_env | curr_instance_id: env.curr_instance_id}} + end + + @spec read(String.t()) :: Type.t() + defp read(input) do + Reader.read_str(input) + end + + @spec eval(Type.t(), Env.t()) :: {Type.t(), Env.t()} + defp eval(ast, env) do + case Env.get(env, "DEBUG-EVAL") do + :error -> + nil + + {:ok, value} when value in [nil, false] -> + nil + + _ -> + IO.puts("EVAL: " <> Printer.pr_str(ast)) + end + + case ast do + {:symbol, symbol} -> + {Env.get!(env, symbol), env} + + {:list, values = [_ | _], _meta} -> + eval_list(values, env) + + {:vector, values, _meta} -> + {values, env} = + Enum.reduce(values, {[], env}, fn value, {values, env} -> + {result, new_env} = eval(value, env) + {[result | values], %{new_env | curr_instance_id: env.curr_instance_id}} + end) + + {values |> Enum.reverse() |> Core.make_vector(), env} + + {:map, pairs, _meta} -> + {pairs, env} = + Enum.reduce(pairs, {%{}, env}, fn {key, value}, {pairs, env} -> + {result, new_env} = eval(value, env) + {Map.put(pairs, key, result), %{new_env | curr_instance_id: env.curr_instance_id}} + end) + + {Core.make_map(pairs), env} + + ast -> + {ast, env} + end + end + + @spec eval_list(nonempty_list(Type.t()), Env.t()) :: {Type.t(), Env.t()} + defp eval_list([{:symbol, "def!"} | rest], env) do + [{:symbol, key}, value] = rest + + curr_instance_id = env.curr_instance_id + {value, env} = eval(value, env) + {value, Env.set(%{env | curr_instance_id: curr_instance_id}, key, value)} + end + + defp eval_list([{:symbol, "let*"} | rest], env) do + [bindings, exprs] = rest + + inner_env = + env + |> Env.new_instance(env.curr_instance_id) + |> parse_let_bindings(unwrap_seq(bindings)) + + eval(exprs, inner_env) + end + + defp eval_list([{:symbol, "do"} | rest], env) do + eval_do(rest, env) + end + + defp eval_list([{:symbol, "if"} | rest], env) do + [condition, if_true | rest] = rest + + {result, new_env} = eval(condition, env) + env = %{new_env | curr_instance_id: env.curr_instance_id} + + if result in [nil, false] do + case rest do + [if_false] -> eval(if_false, env) + [] -> {nil, env} + end + else + eval(if_true, env) + end + end + + defp eval_list([{:symbol, "fn*"} | rest], env) do + [argnames, body] = rest + + func = %Function{ + body: body, + argnames: argnames, + env_id: env.curr_instance_id, + builtin: fn args, call_env -> + call(call_env, env.curr_instance_id, argnames, args, body) + end + } + + {func, env} + end + + defp eval_list([{:symbol, "quote"} | rest], env) do + [arg] = rest + {arg, env} + end + + defp eval_list([{:symbol, "quasiquote"} | rest], env) do + [arg] = rest + eval(Core.quasiquote(arg), env) + end + + defp eval_list([{:symbol, "defmacro!"} | rest], env) do + [{:symbol, key}, value] = rest + + curr_instance_id = env.curr_instance_id + {%Function{} = value, env} = eval(value, env) + value = %{value | is_macro: true} + + {value, Env.set(%{env | curr_instance_id: curr_instance_id}, key, value)} + end + + defp eval_list([func | args], env) do + {func, new_env} = eval(func, env) + env = %{new_env | curr_instance_id: env.curr_instance_id} + + case func do + %Function{is_macro: true} -> + {result, new_env} = call(env, func.env_id, func.argnames, args, func.body) + eval(result, %{new_env | curr_instance_id: env.curr_instance_id}) + + %Function{body: nil} -> + {args, env} = eval_call_args(args, env) + func.builtin.(args, env) + + %Function{} -> + {args, env} = eval_call_args(args, env) + call(env, func.env_id, func.argnames, args, func.body) + + _ -> + raise EvalError, + message: "Expected function, got " <> Printer.pr_str(func, env) + end + end + + @spec unwrap_seq(Type.mal_list() | Type.vector()) :: [Type.t()] + defp unwrap_seq({type, values, _meta}) when type in [:list, :vector], do: values + + @spec parse_let_bindings(Env.t(), [Type.t()]) :: Env.t() + defp parse_let_bindings(env, []), do: env + + defp parse_let_bindings(env, [{:symbol, key}, value | rest]) do + {value, new_env} = eval(value, env) + + %{new_env | curr_instance_id: env.curr_instance_id} + |> Env.set(key, value) + |> parse_let_bindings(rest) + end + + @spec eval_do([Type.t()], Env.t()) :: {Type.t(), Env.t()} + defp eval_do([], env), do: {nil, env} + + defp eval_do([ast], env), do: eval(ast, env) + + defp eval_do([ast | rest], env) do + {_result, new_env} = eval(ast, env) + eval_do(rest, %{new_env | curr_instance_id: env.curr_instance_id}) + end + + @spec call( + env :: Env.t(), + outer_env_id :: Env.Instance.id(), + argnames :: Type.t(), + args :: [Type.t()], + body :: Type.t() + ) :: {Type.t(), Env.t()} + defp call(env, outer_env_id, argnames, args, body) do + inner_env = Env.new_instance(env, outer_env_id, unwrap_seq(argnames), args) + + eval(body, inner_env) + end + + @spec eval_call_args([Type.t()], Env.t()) :: {[Type.t()], Env.t()} + defp eval_call_args(args, env) do + {args, env} = + Enum.reduce(args, {[], env}, fn arg, {args, env} -> + {arg, new_env} = eval(arg, env) + {[arg | args], %{new_env | curr_instance_id: env.curr_instance_id}} + end) + + {Enum.reverse(args), env} + end + + @spec print(Type.t(), Env.t()) :: String.t() + defp print(ast, env) do + Printer.pr_str(ast, env) + end +end diff --git a/impls/elixir.2/lib/steps/step9_try.ex b/impls/elixir.2/lib/steps/step9_try.ex new file mode 100644 index 0000000000..d67260f61f --- /dev/null +++ b/impls/elixir.2/lib/steps/step9_try.ex @@ -0,0 +1,333 @@ +defmodule Mal.Step9Try do + alias Mal.Core + alias Mal.EmptyStatementError + alias Mal.Env + alias Mal.EvalError + alias Mal.Type.Function + alias Mal.ParseError + alias Mal.Printer + alias Mal.Reader + alias Mal.ExceptionWrapperError + alias Mal.Type + + @spec start() :: nil + def start() do + env = + Enum.reduce(Core.ns(), Env.new(), fn {key, value}, env -> + Env.set(env, key, value) + end) + + eval_builtin = fn [ast], call_env -> + eval(ast, %{call_env | curr_instance_id: env.curr_instance_id}) + end + + env = Env.set(env, "eval", %Function{builtin: eval_builtin}) + + {_, env} = rep("(def! not (fn* (a) (if a false true)))", env) + + {_, env} = + rep( + "(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \"\nnil)\")))))", + env + ) + + {_, env} = + rep( + "(defmacro! cond (fn* (& xs) (if (> (count xs) 0) (list 'if (first xs) (if (> (count xs) 1) (nth xs 1) (throw \"odd number of forms to cond\")) (cons 'cond (rest (rest xs)))))))", + env + ) + + case System.argv() do + [filename | args] -> + _ = rep("(load-file \"#{filename}\")", Env.set(env, "*ARGV*", Core.make_list(args))) + nil + + [] -> + env + |> Env.set("*ARGV*", Core.make_list([])) + |> loop() + end + end + + @spec loop(Env.t()) :: nil + defp loop(env) do + case IO.gets("user> ") do + :eof -> + nil + + {:error, reason} -> + raise reason + + input -> + {str, env} = + input + |> binary_part(0, byte_size(input) - 1) + |> rep(env) + + IO.puts(str) + + loop(env) + end + rescue + e in ParseError -> + IO.puts("** ParseError: #{e.message}") + loop(env) + + EmptyStatementError -> + loop(env) + + e in EvalError -> + IO.puts("** EvalError: #{e.message}") + loop(env) + + e in ExceptionWrapperError -> + IO.puts("** Uncaught exception: #{Exception.message(e)}") + loop(env) + + e -> + Exception.format(:error, e, __STACKTRACE__) + |> IO.puts() + + loop(env) + end + + @spec rep(String.t(), Env.t()) :: {String.t(), Env.t()} + defp rep(input, env) do + {ast, new_env} = + input + |> read() + |> eval(env) + + {print(ast, new_env), %{new_env | curr_instance_id: env.curr_instance_id}} + end + + @spec read(String.t()) :: Type.t() + defp read(input) do + Reader.read_str(input) + end + + @spec eval(Type.t(), Env.t()) :: {Type.t(), Env.t()} + defp eval(ast, env) do + case Env.get(env, "DEBUG-EVAL") do + :error -> + nil + + {:ok, value} when value in [nil, false] -> + nil + + _ -> + IO.puts("EVAL: " <> Printer.pr_str(ast)) + end + + case ast do + {:symbol, symbol} -> + {Env.get!(env, symbol), env} + + {:list, values = [_ | _], _meta} -> + eval_list(values, env) + + {:vector, values, _meta} -> + {values, env} = + Enum.reduce(values, {[], env}, fn value, {values, env} -> + {result, new_env} = eval(value, env) + {[result | values], %{new_env | curr_instance_id: env.curr_instance_id}} + end) + + {values |> Enum.reverse() |> Core.make_vector(), env} + + {:map, pairs, _meta} -> + {pairs, env} = + Enum.reduce(pairs, {%{}, env}, fn {key, value}, {pairs, env} -> + {result, new_env} = eval(value, env) + {Map.put(pairs, key, result), %{new_env | curr_instance_id: env.curr_instance_id}} + end) + + {Core.make_map(pairs), env} + + ast -> + {ast, env} + end + end + + @spec eval_list(nonempty_list(Type.t()), Env.t()) :: {Type.t(), Env.t()} + defp eval_list([{:symbol, "def!"} | rest], env) do + [{:symbol, key}, value] = rest + + curr_instance_id = env.curr_instance_id + {value, env} = eval(value, env) + {value, Env.set(%{env | curr_instance_id: curr_instance_id}, key, value)} + end + + defp eval_list([{:symbol, "let*"} | rest], env) do + [bindings, exprs] = rest + + inner_env = + env + |> Env.new_instance(env.curr_instance_id) + |> parse_let_bindings(unwrap_seq(bindings)) + + eval(exprs, inner_env) + end + + defp eval_list([{:symbol, "do"} | rest], env) do + eval_do(rest, env) + end + + defp eval_list([{:symbol, "if"} | rest], env) do + [condition, if_true | rest] = rest + + {result, new_env} = eval(condition, env) + env = %{new_env | curr_instance_id: env.curr_instance_id} + + if result in [nil, false] do + case rest do + [if_false] -> eval(if_false, env) + [] -> {nil, env} + end + else + eval(if_true, env) + end + end + + defp eval_list([{:symbol, "fn*"} | rest], env) do + [argnames, body] = rest + + func = %Function{ + body: body, + argnames: argnames, + env_id: env.curr_instance_id, + builtin: fn args, call_env -> + call(call_env, env.curr_instance_id, argnames, args, body) + end + } + + {func, env} + end + + defp eval_list([{:symbol, "quote"} | rest], env) do + [arg] = rest + {arg, env} + end + + defp eval_list([{:symbol, "quasiquote"} | rest], env) do + [arg] = rest + eval(Core.quasiquote(arg), env) + end + + defp eval_list([{:symbol, "defmacro!"} | rest], env) do + [{:symbol, key}, value] = rest + + curr_instance_id = env.curr_instance_id + {%Function{} = value, env} = eval(value, env) + value = %{value | is_macro: true} + + {value, Env.set(%{env | curr_instance_id: curr_instance_id}, key, value)} + end + + defp eval_list([{:symbol, "try*"} | rest], env) do + case rest do + [try_expr] -> + eval(try_expr, env) + + [try_expr, {:list, [{:symbol, "catch*"}, {:symbol, exc_var}, catch_expr], nil}] -> + try do + eval(try_expr, env) + rescue + e in [ParseError, EmptyStatementError, EvalError] -> + eval_catch(exc_var, Exception.message(e), catch_expr, env) + + e in ExceptionWrapperError -> + eval_catch(exc_var, e.value, catch_expr, env) + end + + _ -> + raise EvalError, message: "malformed try expression" + end + end + + defp eval_list([func | args], env) do + {func, new_env} = eval(func, env) + env = %{new_env | curr_instance_id: env.curr_instance_id} + + case func do + %Function{is_macro: true} -> + {result, new_env} = call(env, func.env_id, func.argnames, args, func.body) + eval(result, %{new_env | curr_instance_id: env.curr_instance_id}) + + %Function{body: nil} -> + {args, env} = eval_call_args(args, env) + func.builtin.(args, env) + + %Function{} -> + {args, env} = eval_call_args(args, env) + call(env, func.env_id, func.argnames, args, func.body) + + _ -> + raise EvalError, + message: "Expected function, got " <> Printer.pr_str(func, env) + end + end + + @spec unwrap_seq(Type.mal_list() | Type.vector()) :: [Type.t()] + defp unwrap_seq({type, values, _meta}) when type in [:list, :vector], do: values + + @spec parse_let_bindings(Env.t(), [Type.t()]) :: Env.t() + defp parse_let_bindings(env, []), do: env + + defp parse_let_bindings(env, [{:symbol, key}, value | rest]) do + {value, new_env} = eval(value, env) + + %{new_env | curr_instance_id: env.curr_instance_id} + |> Env.set(key, value) + |> parse_let_bindings(rest) + end + + @spec eval_do([Type.t()], Env.t()) :: {Type.t(), Env.t()} + defp eval_do([], env), do: {nil, env} + + defp eval_do([ast], env), do: eval(ast, env) + + defp eval_do([ast | rest], env) do + {_result, new_env} = eval(ast, env) + eval_do(rest, %{new_env | curr_instance_id: env.curr_instance_id}) + end + + @spec call( + env :: Env.t(), + outer_env_id :: Env.Instance.id(), + argnames :: Type.t(), + args :: [Type.t()], + body :: Type.t() + ) :: {Type.t(), Env.t()} + defp call(env, outer_env_id, argnames, args, body) do + inner_env = Env.new_instance(env, outer_env_id, unwrap_seq(argnames), args) + + eval(body, inner_env) + end + + @spec eval_call_args([Type.t()], Env.t()) :: {[Type.t()], Env.t()} + defp eval_call_args(args, env) do + {args, env} = + Enum.reduce(args, {[], env}, fn arg, {args, env} -> + {arg, new_env} = eval(arg, env) + {[arg | args], %{new_env | curr_instance_id: env.curr_instance_id}} + end) + + {Enum.reverse(args), env} + end + + @spec eval_catch(String.t(), Type.t(), Type.t(), Env.t()) :: {Type.t(), Env.t()} + defp eval_catch(exc_var, value, catch_expr, env) do + inner_env = + env + |> Env.new_instance(env.curr_instance_id) + |> Env.set(exc_var, value) + + eval(catch_expr, inner_env) + end + + @spec print(Type.t(), Env.t()) :: String.t() + defp print(ast, env) do + Printer.pr_str(ast, env) + end +end diff --git a/impls/elixir.2/lib/steps/stepA_mal.ex b/impls/elixir.2/lib/steps/stepA_mal.ex new file mode 100644 index 0000000000..40cc27df94 --- /dev/null +++ b/impls/elixir.2/lib/steps/stepA_mal.ex @@ -0,0 +1,338 @@ +defmodule Mal.StepaMal do + alias Mal.Core + alias Mal.EmptyStatementError + alias Mal.Env + alias Mal.EvalError + alias Mal.Type.Function + alias Mal.ParseError + alias Mal.Printer + alias Mal.Reader + alias Mal.ExceptionWrapperError + alias Mal.Type + + @spec start() :: nil + def start() do + env = + Enum.reduce(Core.ns(), Env.new(), fn {key, value}, env -> + Env.set(env, key, value) + end) + + eval_builtin = fn [ast], call_env -> + eval(ast, %{call_env | curr_instance_id: env.curr_instance_id}) + end + + env = + env + |> Env.set("eval", %Function{builtin: eval_builtin}) + |> Env.set("*host-language*", "elixir") + + {_, env} = rep("(def! not (fn* (a) (if a false true)))", env) + + {_, env} = + rep( + "(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \"\nnil)\")))))", + env + ) + + {_, env} = + rep( + "(defmacro! cond (fn* (& xs) (if (> (count xs) 0) (list 'if (first xs) (if (> (count xs) 1) (nth xs 1) (throw \"odd number of forms to cond\")) (cons 'cond (rest (rest xs)))))))", + env + ) + + case System.argv() do + [filename | args] -> + _ = rep("(load-file \"#{filename}\")", Env.set(env, "*ARGV*", Core.make_list(args))) + nil + + [] -> + {_, env} = rep("(println (str \"Mal [\" *host-language* \"]\"))", env) + + env + |> Env.set("*ARGV*", Core.make_list([])) + |> loop() + end + end + + @spec loop(Env.t()) :: nil + defp loop(env) do + case IO.gets("user> ") do + :eof -> + nil + + {:error, reason} -> + raise reason + + input -> + {str, env} = + input + |> binary_part(0, byte_size(input) - 1) + |> rep(env) + + IO.puts(str) + + loop(env) + end + rescue + e in ParseError -> + IO.puts("** ParseError: #{e.message}") + loop(env) + + EmptyStatementError -> + loop(env) + + e in EvalError -> + IO.puts("** EvalError: #{e.message}") + loop(env) + + e in ExceptionWrapperError -> + IO.puts("** Uncaught exception: #{Exception.message(e)}") + loop(env) + + e -> + Exception.format(:error, e, __STACKTRACE__) + |> IO.puts() + + loop(env) + end + + @spec rep(String.t(), Env.t()) :: {String.t(), Env.t()} + defp rep(input, env) do + {ast, new_env} = + input + |> read() + |> eval(env) + + {print(ast, new_env), %{new_env | curr_instance_id: env.curr_instance_id}} + end + + @spec read(String.t()) :: Type.t() + defp read(input) do + Reader.read_str(input) + end + + @spec eval(Type.t(), Env.t()) :: {Type.t(), Env.t()} + defp eval(ast, env) do + case Env.get(env, "DEBUG-EVAL") do + :error -> + nil + + {:ok, value} when value in [nil, false] -> + nil + + _ -> + IO.puts("EVAL: " <> Printer.pr_str(ast)) + end + + case ast do + {:symbol, symbol} -> + {Env.get!(env, symbol), env} + + {:list, values = [_ | _], _meta} -> + eval_list(values, env) + + {:vector, values, _meta} -> + {values, env} = + Enum.reduce(values, {[], env}, fn value, {values, env} -> + {result, new_env} = eval(value, env) + {[result | values], %{new_env | curr_instance_id: env.curr_instance_id}} + end) + + {values |> Enum.reverse() |> Core.make_vector(), env} + + {:map, pairs, _meta} -> + {pairs, env} = + Enum.reduce(pairs, {%{}, env}, fn {key, value}, {pairs, env} -> + {result, new_env} = eval(value, env) + {Map.put(pairs, key, result), %{new_env | curr_instance_id: env.curr_instance_id}} + end) + + {Core.make_map(pairs), env} + + ast -> + {ast, env} + end + end + + @spec eval_list(nonempty_list(Type.t()), Env.t()) :: {Type.t(), Env.t()} + defp eval_list([{:symbol, "def!"} | rest], env) do + [{:symbol, key}, value] = rest + + curr_instance_id = env.curr_instance_id + {value, env} = eval(value, env) + {value, Env.set(%{env | curr_instance_id: curr_instance_id}, key, value)} + end + + defp eval_list([{:symbol, "let*"} | rest], env) do + [bindings, exprs] = rest + + inner_env = + env + |> Env.new_instance(env.curr_instance_id) + |> parse_let_bindings(unwrap_seq(bindings)) + + eval(exprs, inner_env) + end + + defp eval_list([{:symbol, "do"} | rest], env) do + eval_do(rest, env) + end + + defp eval_list([{:symbol, "if"} | rest], env) do + [condition, if_true | rest] = rest + + {result, new_env} = eval(condition, env) + env = %{new_env | curr_instance_id: env.curr_instance_id} + + if result in [nil, false] do + case rest do + [if_false] -> eval(if_false, env) + [] -> {nil, env} + end + else + eval(if_true, env) + end + end + + defp eval_list([{:symbol, "fn*"} | rest], env) do + [argnames, body] = rest + + func = %Function{ + body: body, + argnames: argnames, + env_id: env.curr_instance_id, + builtin: fn args, call_env -> + call(call_env, env.curr_instance_id, argnames, args, body) + end + } + + {func, env} + end + + defp eval_list([{:symbol, "quote"} | rest], env) do + [arg] = rest + {arg, env} + end + + defp eval_list([{:symbol, "quasiquote"} | rest], env) do + [arg] = rest + eval(Core.quasiquote(arg), env) + end + + defp eval_list([{:symbol, "defmacro!"} | rest], env) do + [{:symbol, key}, value] = rest + + curr_instance_id = env.curr_instance_id + {%Function{} = value, env} = eval(value, env) + value = %{value | is_macro: true} + + {value, Env.set(%{env | curr_instance_id: curr_instance_id}, key, value)} + end + + defp eval_list([{:symbol, "try*"} | rest], env) do + case rest do + [try_expr] -> + eval(try_expr, env) + + [try_expr, {:list, [{:symbol, "catch*"}, {:symbol, exc_var}, catch_expr], nil}] -> + try do + eval(try_expr, env) + rescue + e in [ParseError, EmptyStatementError, EvalError] -> + eval_catch(exc_var, Exception.message(e), catch_expr, env) + + e in ExceptionWrapperError -> + eval_catch(exc_var, e.value, catch_expr, env) + end + + _ -> + raise EvalError, message: "malformed try expression" + end + end + + defp eval_list([func | args], env) do + {func, new_env} = eval(func, env) + env = %{new_env | curr_instance_id: env.curr_instance_id} + + case func do + %Function{is_macro: true} -> + {result, new_env} = call(env, func.env_id, func.argnames, args, func.body) + eval(result, %{new_env | curr_instance_id: env.curr_instance_id}) + + %Function{body: nil} -> + {args, env} = eval_call_args(args, env) + func.builtin.(args, env) + + %Function{} -> + {args, env} = eval_call_args(args, env) + call(env, func.env_id, func.argnames, args, func.body) + + _ -> + raise EvalError, + message: "Expected function, got " <> Printer.pr_str(func, env) + end + end + + @spec unwrap_seq(Type.mal_list() | Type.vector()) :: [Type.t()] + defp unwrap_seq({type, values, _meta}) when type in [:list, :vector], do: values + + @spec parse_let_bindings(Env.t(), [Type.t()]) :: Env.t() + defp parse_let_bindings(env, []), do: env + + defp parse_let_bindings(env, [{:symbol, key}, value | rest]) do + {value, new_env} = eval(value, env) + + %{new_env | curr_instance_id: env.curr_instance_id} + |> Env.set(key, value) + |> parse_let_bindings(rest) + end + + @spec eval_do([Type.t()], Env.t()) :: {Type.t(), Env.t()} + defp eval_do([], env), do: {nil, env} + + defp eval_do([ast], env), do: eval(ast, env) + + defp eval_do([ast | rest], env) do + {_result, new_env} = eval(ast, env) + eval_do(rest, %{new_env | curr_instance_id: env.curr_instance_id}) + end + + @spec call( + env :: Env.t(), + outer_env_id :: Env.Instance.id(), + argnames :: Type.t(), + args :: [Type.t()], + body :: Type.t() + ) :: {Type.t(), Env.t()} + defp call(env, outer_env_id, argnames, args, body) do + inner_env = Env.new_instance(env, outer_env_id, unwrap_seq(argnames), args) + + eval(body, inner_env) + end + + @spec eval_call_args([Type.t()], Env.t()) :: {[Type.t()], Env.t()} + defp eval_call_args(args, env) do + {args, env} = + Enum.reduce(args, {[], env}, fn arg, {args, env} -> + {arg, new_env} = eval(arg, env) + {[arg | args], %{new_env | curr_instance_id: env.curr_instance_id}} + end) + + {Enum.reverse(args), env} + end + + @spec eval_catch(String.t(), Type.t(), Type.t(), Env.t()) :: {Type.t(), Env.t()} + defp eval_catch(exc_var, value, catch_expr, env) do + inner_env = + env + |> Env.new_instance(env.curr_instance_id) + |> Env.set(exc_var, value) + + eval(catch_expr, inner_env) + end + + @spec print(Type.t(), Env.t()) :: String.t() + defp print(ast, env) do + Printer.pr_str(ast, env) + end +end diff --git a/impls/elixir.2/mix.exs b/impls/elixir.2/mix.exs new file mode 100644 index 0000000000..b978b1dd0a --- /dev/null +++ b/impls/elixir.2/mix.exs @@ -0,0 +1,42 @@ +defmodule Mal.MixProject do + use Mix.Project + + def project do + [ + app: :mal, + version: "0.1.0", + elixir: "~> 1.14", + start_permanent: Mix.env() == :prod, + deps: deps(), + aliases: aliases(), + dialyzer: [ + flags: ["-Wextra_return", "-Wmissing_return", "-Wunderspecs", "-Wunmatched_returns"], + plt_add_apps: [:iex] + ] + ] + end + + def application do + [ + mod: {Mal, []}, + extra_applications: [:logger] + ] + end + + defp deps do + [ + {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, + {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false} + ] + end + + defp aliases do + [ + check: [ + "format --check-formatted", + "credo", + "dialyzer" + ] + ] + end +end diff --git a/impls/elixir.2/mix.lock b/impls/elixir.2/mix.lock new file mode 100644 index 0000000000..8f9dae8278 --- /dev/null +++ b/impls/elixir.2/mix.lock @@ -0,0 +1,8 @@ +%{ + "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, + "credo": {:hex, :credo, "1.7.16", "a9f1389d13d19c631cb123c77a813dbf16449a2aebf602f590defa08953309d4", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "d0562af33756b21f248f066a9119e3890722031b6d199f22e3cf95550e4f1579"}, + "dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"}, + "erlex": {:hex, :erlex, "0.2.8", "cd8116f20f3c0afe376d1e8d1f0ae2452337729f68be016ea544a72f767d9c12", [:mix], [], "hexpm", "9d66ff9fedf69e49dc3fd12831e12a8a37b76f8651dd21cd45fcf5561a8a7590"}, + "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, +} diff --git a/impls/elixir.2/run b/impls/elixir.2/run new file mode 100755 index 0000000000..d3444cfd10 --- /dev/null +++ b/impls/elixir.2/run @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +cd "$(dirname "$0")" || exit +# -e '' is necessary so that the elixir version in Ubuntu 24.04 +# (14) doesn't parse the first arg as the script path +exec env STEP="${STEP:-stepA_mal}" mix run -e '' -- "${@}"