Current section
Files
Jump to
Current section
Files
src/otpcl_eval.erl
% @doc OTPCL interpreter/evaluator.
%
% OTPCL's interpreter effectively revolves around repeatedly calling 2-arity
% functions ("commands"), the first argument being the actual list of arguments
% for that function/command, and the second being the current interpretation
% state (expressed as a tuple of two maps, one with all command definitions and
% one with all variable definitions). Each command-backing function in turn
% returns a tuple with a return value and an updated state.
%
% To illustrate: when OTPCL's parser encounters the command invocation `foo bar
% baz' and sends the corresponding parse tree to the interpreter, the
% interpreter in turn calls `{Result, NewState} = Fun([bar, baz], State)' (where
% `Fun' is the value of the `foo' key in the first element of the `State' tuple,
% `Result' is the return value for that command, and `NewState' is the updated
% state).
%
% This means it's pretty straightforward to define an OTPCL command yourself
% from within Erlang: simply define a 2-arity function where the first argument
% is a list and the second argument is a 2-element tuple of maps. A module that
% defines OTPCL commands can/should specify which functions in that module are
% "OTPCL-aware" in this fashion like so:
%
% ```
% -module(my_otpcl_cmds).
% -export([foo/2, bar/2, baz/2]).
% -otpcl_cmds([foo, bar, baz]). % OTPCL-aware funs in the module
%
% foo([Thing], State) ->
% {ok, State}.
% bar([Thing1, Thing2], State) ->
% {{Thing1, Thing2}, State}.
% baz([Name, Val], State) ->
% otpcl_stdlib:set([Name, Val], State).
% '''
%
% The interpreter itself is also an OTPCL-aware function in this sense (albeit
% with a simplification in that it does not <em>require</em> its first argument
% to be a list; it can take a parse tree directly). It can thus be invoked from
% within OTPCL:
%
% ```
% otpcl> import otpcl_eval
% {ok,otpcl_eval}
% otpcl> eval {set foo "howdy~n"}
% ok
% otpcl> print $foo
% howdy
% ok
% otpcl> import otpcl_env
% ok
% otpcl> eval {
% ...> set foo "aloha~n"
% ...> print $foo
% ...> } [default_state]
% aloha
% [ ... pile of interpreter state ... ]
% otpcl> print $foo
% howdy
% ok
% '''
%
% In fact, most OTPCL features are in turn implemented as OTPCL-aware
% command-backing functions; that is: OTPCL exposes its own functionality as
% OTPCL commands wherever it's possible/practical to do so.
%
% Of course, one may also do this from any OTP application that uses OTPCL,
% e.g. one written in Erlang:
%
% ```
% erl> State0 = otpcl_env:default_state().
% [ ... pile of interpreter state ... ]
% erl> {ok, State1} = otpcl_stdlib:set([foo, <<"howdy~n">>], State0).
% [ ... pile of interpreter state ... ]
% erl> {ok, State2} = otpcl_eval:eval("print $foo", State1).
% howdy
% [ ... pile of interpreter state ... ]
% '''
-module(otpcl_eval).
-include("otpcl.hrl").
-export([interpret/1, interpret/2, eval/1, eval/2, eval_file/1, eval_file/2,
make_charstring/1, make_binstring/1, make_atomic/1, make_atom/1]).
-otpcl_cmds([interpret, eval, eval_file]).
-ifdef(DEBUG).
-define(DEBUG_PRINT(Msg, Args), io:format(Msg, Args)).
-else.
-define(DEBUG_PRINT(Msg, Args), ok).
-endif.
% Build stuff out of tokens
-spec make_charstring([token()]) -> string().
% @doc Extract a character string from a token string.
make_charstring(Tokens) ->
[C || {C,_} <- Tokens].
-spec make_binstring([token()]) -> binary().
% @doc Extract a binary string from a token string.
make_binstring(Tokens) ->
list_to_binary(make_charstring(Tokens)).
-spec make_atomic([token()]) -> atom() | integer() | float().
% @doc Extract a float, integer, or atom (in order of preference) from a token
% string.
make_atomic(Tokens) ->
Text = make_charstring(Tokens),
make_atomic(Text, float, string:to_float(Text)).
% Floats
make_atomic(_, float, {Float, []}) ->
Float;
make_atomic(Text, float, _) ->
make_atomic(Text, integer, string:to_integer(Text));
% Integers (if this conversion attempt fails, then we just treat it as
% an ordinary atom)
make_atomic(_, integer, {Int, []}) ->
Int;
make_atomic(Text, integer, _) ->
list_to_atom(Text).
-spec make_atom([token()]) -> atom().
% @doc Extract an atom from a token string. This skips any attempt to check if
% an atom is a number (which means single-quoted atoms might technically be more
% efficient than unquoted atoms at the moment...).
make_atom(Tokens) ->
list_to_atom(make_charstring(Tokens)).
% Here's the meat of the interpreter.
-spec interpret(tree() | [tree()]) -> eval_success() | eval_error().
% @doc Interpret the parse nodes with the default OTPCL starting state.
interpret(Nodes) ->
interpret(Nodes, otpcl_env:default_state()).
-spec interpret(tree() | [tree()], state()) -> eval_success() | eval_error().
% @doc Interpret the parse nodes with a custom starting state.
interpret({parsed, unquoted, Tokens}, _State) ->
make_atomic(Tokens);
interpret({parsed, single_quoted, Tokens}, _State) ->
make_atom(Tokens);
interpret({parsed, double_quoted, Tokens}, _State) ->
make_binstring(Tokens); % TODO: allow var/funcall substitution (maybe?)
interpret({parsed, braced, Tokens}, _State) ->
make_binstring(Tokens);
interpret({parsed, backquoted, Tokens}, _State) ->
make_charstring(Tokens);
interpret({parsed, var_unquoted, Tokens}, State) ->
interpret({parsed, var, Tokens}, State);
interpret({parsed, var_braced, Tokens}, State) ->
interpret({parsed, var, Tokens}, State);
interpret({parsed, var, Tokens}, State) ->
{Val, State} = otpcl_meta:get([make_atom(Tokens)], State),
Val;
% FIXME: any state changes here (new/modified functions and variables,
% for example) won't actually persist beyond a list/tuple/funcall
% literal until I define some better logic here. This might end up
% being a "feature", though.
interpret({parsed, list, Items}, State) ->
[interpret(I, State) || I <- Items];
interpret({parsed, tuple, Items}, State) ->
list_to_tuple([interpret(I, State) || I <- Items]);
interpret({parsed, funcall, Words}, State) ->
Cmd = [interpret(I, State) || I <- Words],
{Res, _} = otpcl_meta:apply(Cmd, State),
Res;
interpret({parsed, command, []}, State) ->
otpcl_meta:get(['RETVAL'], State);
interpret({parsed, command, Words}, State) ->
Cmd = [interpret(I, State) || I <- Words],
otpcl_meta:apply(Cmd, State);
interpret({parsed, comment, _}, State) ->
otpcl_meta:get(['RETVAL'], State);
interpret({parsed, program, [Cmd|Rest]}, State) ->
{RetVal, NewState} = interpret(Cmd, State),
{ok, RetState} = otpcl_meta:set(['RETVAL', RetVal], NewState),
interpret({parsed, program, Rest}, RetState);
interpret({parsed, program, []}, State) ->
otpcl_meta:get(['RETVAL'], State);
interpret({parsed, Type, Data}, State) ->
{error, {unknown_node_type, Type, Data}, State};
interpret([{parsed, Type, Data}], State) ->
interpret({parsed, Type, Data}, State);
interpret(InvalidNode, State) ->
{error, {not_an_otpcl_parse_node, InvalidNode}, State}.
% And some nice friendly wrappers around that interpreter
-spec eval(eval_input()) -> eval_success() | eval_error().
% @doc Evaluate a string with the default OTPCL starting state.
eval(Src) ->
eval(Src, otpcl_env:default_state()).
-spec eval(eval_input(), state()) -> eval_success() | eval_error().
% @doc Evaluate a string with a custom starting state.
eval(Src = [Char|_], State) when is_integer(Char) ->
eval([Src], State);
eval([Src, SubState], State) ->
{eval([Src], SubState), State};
eval([Src], State) ->
{ok, Tree, []} = otpcl_parse:parse(Src),
interpret(Tree, State);
eval(Src, State) ->
eval([Src], State).
-spec eval_file(filename()) -> eval_success() | eval_error().
% @doc Evaluate the named file with the default OTPCL starting state.
eval_file(Filename) ->
eval_file(Filename, otpcl_env:default_state()).
-spec eval_file(filename(), state()) -> eval_success() | eval_error().
% @doc Evaluate the named file with a custom starting state.
eval_file(Filename = [Char|_], State) when is_integer(Char) ->
eval_file([Filename], State);
eval_file([Filename], State) ->
{ok, Src} = file:read_file(Filename),
Tokens = otpcl_parse:scan(Src, otpcl_parse:initpos(Filename)),
{ok, Tree, []} = otpcl_parse:parse(Tokens),
interpret(Tree, State);
eval_file(Filename, State) ->
eval_file([Filename], State).