Skip to content

Instantly share code, notes, and snippets.

@samrat
Last active August 16, 2016 14:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save samrat/c625f19d6a128115343d91145957abac to your computer and use it in GitHub Desktop.
Save samrat/c625f19d6a128115343d91145957abac to your computer and use it in GitHub Desktop.
A small Lisp interpreter in Erlang
-module(lisp).
-export([repl/0]).
-type expr() :: {ident, string()}
| {int, integer()}
| {bool, boolean()}
| {list, [expr()]}
| no_match.
-type token() :: {ident, string()}
| {int, integer()}
| lparen | rparen
| eos.
is_alpha(Ch) -> ($a =< Ch andalso Ch =< $z).
is_digit(Ch) -> ($0 =< Ch andalso Ch =< $9).
-spec get_while(fun((T) -> boolean()),[T]) -> {[T],[T]}.
get_while(P,[Ch|Rest]) ->
case P(Ch) of
true ->
{Succeeds,Remainder} = get_while(P,Rest),
{[Ch|Succeeds],Remainder};
false ->
{[],[Ch|Rest]}
end;
get_while(_P,[]) ->
{[],[]}.
-spec get_token(string()) -> {token(), string()}.
get_token([]) -> eos;
get_token([$ |Rest]) -> get_token(Rest);
get_token([$(|Rest]) -> {lparen, Rest};
get_token([$)|Rest]) -> {rparen, Rest};
get_token([Ch|Rest]) when ($a =< Ch andalso Ch =< $z) orelse
Ch == $+ orelse
Ch == $- ->
{Rest_Ident, Rest_String} = get_while(fun(X) -> is_alpha(X) orelse
is_digit(X) end,
Rest),
{{ident, [Ch|Rest_Ident]}, Rest_String};
get_token([Ch|Rest]) when $0 =< Ch andalso Ch =< $9 ->
{Rest_Digit, Rest_String} = get_while(fun is_digit/1, Rest),
{{int, list_to_integer([Ch|Rest_Digit])}, Rest_String}.
%% Parser
-spec get_expr(string()) -> {expr(), string()}.
get_expr(Str) ->
case get_token(Str) of
{{ident, Ident}, Rest} -> {{ident, Ident}, Rest};
{{int, Int}, Rest} -> {{int, Int}, Rest};
{lparen, _Rest} -> get_list(Str);
{_, Rest} -> {no_match, Rest}
end.
get_list(Str) ->
{lparen, R1} = get_token(Str),
{Seq, R2} = get_seq(R1),
{rparen, R3} = get_token(R2),
{{list, Seq}, R3}.
get_seq(Str) ->
case get_expr(Str) of
{no_match, Rest} -> {[], Rest};
{Exp, Rest} -> {E2, R2} = get_token(Rest),
case E2 of
rparen -> {[Exp], Rest};
_ -> {Seq, R3} = get_seq(Rest),
{[Exp] ++ Seq, R3}
end
end.
eval({list, [{ident, "do"}|T]}, Env) ->
lists:foldl(fun(X, _Acc) -> eval(X, Env) end, false, T);
eval({list, [{ident, "let"}|T]}, Env) ->
[{list, Bindings}|Body] = T,
NewEnv = lists:foldl(fun({list, [{ident, Var},Val]},BoundEnv) ->
maps:put(Var, eval(Val, Env),BoundEnv)
end,
Env,
Bindings),
eval({list, [{ident, "do"}|Body]}, NewEnv);
eval({list, [{ident, "define"}|Rest]}, Env) ->
[{ident, VarName}, Val] = Rest,
{newenv, maps:put(VarName, eval(Val, Env), Env)};
eval({list, [{ident, "if"}|T]}, Env) ->
[Cond|R1] = T,
[Then|R2] = R1,
[Else|_R3] = R2,
case eval(Cond, Env) of
true -> eval(Then, Env);
false -> eval(Else, Env)
end;
%% TODO: how do I avoid having to have a `call` keyword? Erlang
%% doesn't support varargs(?)
eval({list, [{ident, "call"},{ident, FnName}|Args]}, Env) ->
FnClosure = maps:get(FnName, Env),
FnClosure(lists:map(fun(X) -> eval(X, Env) end, Args));
eval({list, [{ident, "lambda"}|Rest]}, Env) ->
[{list, Params}|Body] = Rest,
fun(Args) ->
NewEnv = lists:foldl(fun({{ident,Var},Val}, NewEnv) ->
maps:put(Var, Val, NewEnv) end,
Env,
%% TODO this assumes correct num. of args is passed
lists:zip(Params, Args)),
eval({list, [{ident, "do"}|Body]}, NewEnv) end;
eval({list, [{ident,Var}|T]}, Env) -> apply(maps:get(Var, Env),
lists:map(fun(X) -> eval(X, Env) end, T));
eval({ident, "true"}, _) -> true;
eval({ident, "false"}, _) -> false;
eval({ident, Name}, Env) -> maps:get(Name, Env);
eval({int, Val}, _) -> Val;
eval(X, _) -> X.
make_env() -> #{"+" => fun(X,Y) -> X+Y end,
"-" => fun(X,Y) -> X-Y end,
"println" => fun(X) -> io:fwrite("~w~n", [X]) end}.
repl(Env) ->
Input = io:get_line("> "),
%% io:write(Input),
{Parsed, "\n"} = get_expr(Input),
case eval(Parsed, Env) of
{newenv, NewEnv} -> repl(NewEnv);
X -> io:fwrite("~w~n", [X]),
repl(Env)
end.
repl() -> repl(make_env()).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment