Current section
Files
Jump to
Current section
Files
src/gdminus_int.erl
%%%--------------------------------------------------------------------------
%%% @copyright (C) 2021, Lincoln Bryant
%%% @doc GDMinus Interpreter.
%%% This package facilitates interoperability between Godot and Erlang by
%%% implementing a subset of the GDScript scripting language as an Erlang
%%% module. A subset of the GDScript standard library has been made available
%%% as well. Users may additionally define their own functions in Erlang and
%%% make them available to GDMinus programs. This source code is made
%%% available under the Apache License v2.0
%%% @author Lincoln Bryant
%%% @end
%%%--------------------------------------------------------------------------
-module(gdminus_int).
% GDMinus Interpreter
% -------------------
% This module is rather dirty in the sense that it abuses the Erlang process
% dictionary to process the syntax tree of GDMinus rather than threading the
% state through every function.
-export([
init/0,
destroy/0,
file/1, file/2,
do/2, do/1,
insert_function/2,
insert_dict/2,
scan_file/1,
parse_file/1
]).
-record(state, {
curEnv = 0,
curLoop = 0,
envs = maps:new(),
breakers = maps:new(),
appFuns = maps:new(),
console = []
}).
-record(console, {
stdout = [],
stderr = []
}).
-record(env, {functions = maps:new(), variables = maps:new()}).
%%============================================================================
%% API functions
%%============================================================================
-spec init() -> ok.
%%----------------------------------------------------------------------------
%% @doc Initialize the state of the interpreter in the process dictionary
%% @end
%%----------------------------------------------------------------------------
init() ->
erlang:put(state, #state{}),
erlang:put(console, #console{}),
ok.
-spec destroy() -> ok.
%%----------------------------------------------------------------------------
%% @doc Delete all stateful information in the process dictionary
%% @end
%%----------------------------------------------------------------------------
destroy() ->
erlang:erase(state),
erlang:erase(console),
ok.
-spec insert_function(string(), fun()) -> ok.
%%----------------------------------------------------------------------------
%% @doc Insert an function into the GDScript function table. Functions may only
%% take a list as an argument.
%% @end
%%----------------------------------------------------------------------------
insert_function(Name, Fun) ->
St0 = erlang:get(state),
AppFun0 = St0#state.appFuns,
AppFun1 = maps:put(Name, Fun, AppFun0),
logger:notice("Inserted new function ~p", [AppFun1]),
St1 = St0#state{appFuns = AppFun1},
erlang:put(state, St1),
ok.
-spec insert_dict(string(), map()) -> ok.
%%----------------------------------------------------------------------------
%% @doc Inject an Erlang map to a GDScript dictionary at the global scope. The
%% caller is responsible for ensuring types are GDScript-compatible, e.g.
%% no BEAM atoms
%% @end
%%----------------------------------------------------------------------------
insert_dict(Name, Map) ->
global_declare(Name, Map, var).
-spec scan_file(list()) -> list().
%%----------------------------------------------------------------------------
%% @doc Read a file from disk and tokenize it to prepare for parsing.
%% @end
%%----------------------------------------------------------------------------
scan_file(Path) ->
{ok, F} = file:read_file(Path),
Fn = binary:bin_to_list(F),
{ok, Tokens, _L} = gdminus_scan:string(Fn),
% Fix up indents/dedents
gdminus_scan:normalize(Tokens).
-spec parse_file(list()) -> list().
%%----------------------------------------------------------------------------
%% @doc Parse a list of tokens into GDScript grammar.
%% @end
%%----------------------------------------------------------------------------
parse_file(Path) ->
Tokens = scan_file(Path),
{ok, Tree} = gdminus_parse:parse(Tokens),
Tree.
% Return the standard 3-tuple, plus a map with requested variables.
-spec do(list(), map()) -> {list(), list(), map(), tuple()}.
%%----------------------------------------------------------------------------
%% @doc Walk the parse tree, evaluating statements and expressions along the
%% way. Output from print functions etc returned as lists representing
%% Standard Out and Standard Error. A map will be returned with the final
%% state of requested variables. Finally the state record itself will be
%% returned.
%% @end
%%----------------------------------------------------------------------------
do(Stmt, Return) ->
{ok, Tokens, _L} = gdminus_scan:string(Stmt),
% fix up the indents and dedents
NormalForm = gdminus_scan:normalize(Tokens),
{ok, Tree} = gdminus_parse:parse(NormalForm),
walk(Tree),
Stdout = console_get(stdout),
Stderr = console_get(stderr),
R = return(Return),
{Stdout, Stderr, R, erlang:get(state)}.
-spec do(list()) -> {list(), list(), tuple()}.
%%----------------------------------------------------------------------------
%% @doc Walk the parse tree, evaluating statements and expressions along the
%% way. Output from print functions etc returned as lists representing
%% Standard Out and Standard Error. The state record itself will be
%% returned as well.
%% @end
%%----------------------------------------------------------------------------
do(Stmt) ->
{ok, Tokens, _L} = gdminus_scan:string(Stmt),
% fix up the indents and dedents
NormalForm = gdminus_scan:normalize(Tokens),
{ok, Tree} = gdminus_parse:parse(NormalForm),
walk(Tree),
Stdout = console_get(stdout),
Stderr = console_get(stderr),
{Stdout, Stderr, erlang:get(state)}.
%%----------------------------------------------------------------------------
%% @doc Open a file, tokenize, parse, evaluate, and return the final output to
%% the caller.
%% @end
%%----------------------------------------------------------------------------
-spec file(list()) -> {list(), list()}.
file(Path) ->
file(Path, default).
%%----------------------------------------------------------------------------
%% @doc Open a file, tokenize, parse, evaluate, and return the final output to
%% the caller. Optionally return the final state of the program.
%% @end
%%----------------------------------------------------------------------------
-spec file(list(), atom()) -> {list(), list()} | {list(), list(), tuple()}.
file(Path, default) ->
init(),
{ok, F} = file:read_file(Path),
Fn = binary:bin_to_list(F),
{StdOut, StdErr, _State} = do(Fn),
destroy(),
{StdOut, StdErr};
file(Path, debug) ->
init(),
{ok, F} = file:read_file(Path),
Fn = binary:bin_to_list(F),
{StdOut, StdErr, State} = do(Fn),
destroy(),
{StdOut, StdErr, State}.
%%============================================================================
%% Walking the parse tree
%%============================================================================
% Walk the tree, evaluating statements and expressions as it goes.
walk([]) ->
ok;
walk([{Op, _Val1, _Val2} | Rest]) when
Op == '+';
Op == '-';
Op == '*';
Op == '/';
Op == '%';
Op == '==';
Op == '>=';
Op == '<=';
Op == '!';
Op == '>';
Op == '<'
->
% Nothing useful from evaluating these nodes since they are just exprs without
% side-effects
walk(Rest);
walk([{enum, List} | Rest]) ->
enum(List),
walk(Rest);
walk([{const, {name, _L1, Name}, {number, _L2, Val}} | Rest]) ->
declare(Name, Val, var),
walk(Rest);
walk([{var, {name, _L, Name}} | Rest]) ->
declare(Name, var),
walk(Rest);
walk([{var, {name, _L, Name}, {dict, KVItems}} | Rest]) ->
declare(Name, dictionary(KVItems), var),
walk(Rest);
walk([{var, {name, _L, Name}, Val} | Rest]) ->
declare(Name, expr(Val), var),
walk(Rest);
walk([{'=', {kv, Name, Key}, Val} | Rest]) ->
{name, _L, N} = Name,
kv_update(N, expr(Name), expr(Key), expr(Val)),
walk(Rest);
walk([{'=', {name, _L, Name}, Val} | Rest]) ->
assign(Name, expr(Val)),
walk(Rest);
walk([{Op, Expression, Block} | Rest]) when Op == 'if'; Op == 'elif' ->
%TODO: Prevent 'elif' if not preceeded by 'if'
%TODO: Should we case this, or push the Rest into the statement function? Same for while, for..
case ifelse(expr(Expression), Block) of
ok ->
walk(Rest);
X ->
% If there is a return statement nestled in there, we need to get the result out
X
end;
walk([{Op, Block} | Rest]) when Op == 'else' ->
case ifelse(else, Block) of
ok ->
walk(Rest);
X ->
% If there is a return statement nestled in there, we need to get the result out
X
end;
walk([{while, Condition, Block} | Rest]) ->
Expr = expr(Condition),
incrementLoop(),
while(Expr, Condition, Block),
walk(Rest);
walk([{for, {name, _Line, Name}, Iter, Block} | Rest]) ->
Expr = expr(Iter),
incrementLoop(),
for(Name, Expr, Block, Rest);
walk([{Op} | _Rest]) when Op == 'break'; Op == 'continue' ->
St0 = erlang:get(state),
CurLoop = St0#state.curLoop,
case CurLoop >= 1 of
true ->
Breakers = St0#state.breakers,
St1 = St0#state{breakers = maps:put(CurLoop, Op, Breakers)},
erlang:put(state, St1)
end;
walk([{func, {name, _Line, Name}, Args, Block} | Rest]) ->
define(Name, Args, Block),
walk(Rest);
walk([{func_call, Name, Args} | Rest]) ->
expr({func_call, Name, Args}),
walk(Rest);
walk([{return, Expression} | _Rest]) ->
expr(Expression);
walk([{match, Expression, Conditions} | Rest]) ->
match(Expression, Conditions),
walk(Rest);
walk([{return} | _Rest]) ->
'Null'.
%%============================================================================
%% Expressions
%%============================================================================
expr({string, _Line, String}) ->
String;
expr({number, _L, Value}) ->
Value;
expr({name, _L, Variable}) ->
get_variable(Variable);
%TODO : Clean this up - it's become very hacky. The parser can't tell the
% difference between a class method invocation vs a dictionary acess.
expr(
{func_call, {kv, {name, _Line1, Name1}, {string, _Line2, "has"}}, Args}
) ->
function("dict_has", [Name1 | Args]);
expr(
{func_call, {kv, {name, _Line1, Name1}, {string, _Line2, "size"}}, _Args}
) ->
function("dict_size", [Name1]);
expr(
{func_call, {kv, {name, _Line1, Name1}, {string, _Line2, "empty"}}, _Args}
) ->
function("dict_empty", [Name1]);
expr(
{func_call, {kv, {name, _Line1, Name1}, {string, _Line2, Name2}}, Args}
) ->
% Will return a value or null if the function is only called for side
% effects.
function(Name1 ++ "." ++ Name2, Args);
expr({func_call, {name, _Line, Name}, Args}) ->
% Will return a value or null if the function is only called for side
% effects.
function(Name, Args);
expr({'+', Val1, Val2}) ->
overload_add(expr(Val1), expr(Val2));
expr({'-', Val1, Val2}) ->
expr(Val1) - expr(Val2);
expr({'/', Val1, Val2}) ->
expr(Val1) / expr(Val2);
expr({'*', Val1, Val2}) ->
expr(Val1) * expr(Val2);
expr({'%', Val1, Val2}) ->
expr(Val1) rem expr(Val2);
expr({'==', Val1, Val2}) ->
expr(Val1) == expr(Val2);
expr({'!=', Val1, Val2}) ->
expr(Val1) /= expr(Val2);
expr({'>=', Val1, Val2}) ->
expr(Val1) >= expr(Val2);
expr({'<=', Val1, Val2}) ->
expr(Val1) =< expr(Val2);
expr({'>', Val1, Val2}) ->
expr(Val1) > expr(Val2);
expr({'<', Val1, Val2}) ->
expr(Val1) < expr(Val2);
expr({negation, Val}) ->
negate(expr(Val));
expr({dict, List}) ->
% Declaration
dictionary(List);
expr({kv, Name, Key}) ->
keyvalue(expr(Name), expr(Key));
expr(List) when is_list(List) ->
% Arrays
eval(List);
expr(Other) ->
% NOTE: This function allows Erlang terms to leak into GDscript if you
% aren't careful
Other.
negate(Val) when is_number(Val) ->
0 - Val.
% Check to see if a function is built in, otherwise check the function table.
function(Name, Args) ->
case local_function(Name, Args) of
undefined ->
% Not a built-in, see if it's in the application function table
E = eval(Args),
case application_function(Name, E) of
undefined ->
% Not there either, check if it's a built-in
builtin_function(Name, E);
Value ->
Value
end;
Value ->
Value
end.
% Key value access can either be for an array or a dictionary
% which have slightly diff semantics
keyvalue(Name, Key) when is_map(Name) ->
maps:get(Key, Name);
keyvalue(Name, Key) when is_list(Name) ->
% it seems like Arrays should
lists:nth(Key + 1, Name).
%%============================================================================
%% Statements
%%============================================================================
% Declare the variable in the current environment. Behaviour depends on the
% caller - if called from a variable declaration then it checks for shadowing
% and errors if the same variable has already been declared in this scope.
% Otherwise if the caller is a function constructor then we just allow
% shadowing.
declare(Name, Caller) ->
declare(Name, 'Null', Caller).
declare(Name, Val, var) ->
State = erlang:get(state),
Env = State#state.curEnv,
case get_obj(var, Name, Env) of
{_, false} ->
put_obj(var, Name, Val, Env);
_X ->
throw("Variable already exists in the current scope")
end;
declare(Name, Val, func) ->
State = erlang:get(state),
Env = State#state.curEnv,
put_obj(var, Name, Val, Env).
% Special version of declare that always happens at the global scope
global_declare(Name, Val, var) ->
Env = 0,
case get_obj(var, Name, Env) of
{_, false} ->
put_obj(var, Name, Val, Env);
_X ->
throw("Variable already exists in the current scope")
end.
% Assign a value to a variable that exists in the current environment.
assign(Name, Val) ->
State = erlang:get(state),
Env = State#state.curEnv,
case get_obj(var, Name, Env) of
{_Env, false} ->
throw("Variable not declared in the current scope");
{Where, _CurrentValue} ->
% Overwrite the current value
put_obj(var, Name, Val, Where)
end.
dictionary([]) ->
maps:new();
dictionary(List) ->
dictionary(List, maps:new()).
dictionary([], Map) ->
Map;
dictionary([{kv, {_Type1, _Line1, Key}, Val} | T], Map1) ->
Map2 = maps:put(Key, expr(Val), Map1),
dictionary(T, Map2).
% Can update either Dicts or Arrays
kv_update(Name, Map, Key, Val) when is_map(Map) ->
% Updating a dictionary
Map1 = maps:put(Key, Val, Map),
assign(Name, Map1);
kv_update(Name, List, Key, Val) when is_list(List) ->
% TODO: Lists may not be the right way to represent arrays
% internally, since updating an element in the middle of a
% list isn't that great. 3 operations on the list itself to
% generate List1 seems like it would be a performance sap.
Index = lists:nth(Key + 1, List),
List1 =
lists:sublist(List, Index - 1) ++
[Val] ++
lists:nthtail(Index, List),
assign(Name, List1).
% If 'if' or 'elif' evaluate to false, just return. Otherwise if it is true, or
% the statement is an 'else', process the indented block of statements and
% expressions.
ifelse(false, _Block) ->
ok;
ifelse(Op, Block) when Op == 'true'; Op == 'else' ->
% Increase the environment level, walk the block, decrease.
St0 = erlang:get(state),
Env = St0#state.curEnv,
erlang:put(state, St0#state{curEnv = Env + 1}),
Ret = walk(Block),
% Remove the temporary environment and decrement the current state
removeEnv(Env + 1),
St1 = erlang:get(state),
erlang:put(state, St1#state{curEnv = Env}),
Ret.
% Define a new function
define(Name, Params, Block) ->
St0 = erlang:get(state),
Env = St0#state.curEnv,
define(Name, Params, Block, Env).
define(_Name, _Params, _Block, Env) when Env > 0 ->
throw("Functions cannot be declared in this scope");
define(Name, Params, Block, Env) ->
case get_obj(func, Name, Env) of
{Env, false} ->
put_obj(func, Name, {Params, Block}, Env);
_ ->
throw("Function is already defined")
end.
% For loops
for(Name, Iter, Block, Rest) when is_integer(Iter) ->
Expanded = [{number, 'Null', N} || N <- lists:seq(1, Iter)],
for(Name, Expanded, Block, Rest);
for(_Name, [], _Block, Rest) ->
% Loop is finishe, decrement counter
decrementLoop(),
% Walk the rest of the tree
walk(Rest);
for(Name, [Head | Tail] = Iter, Block, Rest) when is_list(Iter) ->
St0 = erlang:get(state),
Env = St0#state.curEnv,
% Create a new environment for the loop to execute in
erlang:put(state, St0#state{curEnv = Env + 1}),
% Grab the iterator and declare it within an environment
setup_function_env([{Head, {name, 'Null', Name}}]),
% false -> No breaks, so proceed as normal.
% 1. Walk the block
% 2. Remove the env
% 3. Decrement current Env
% 4. Proceed to next loop
% 'continue' -> Jump to the next iteration in the loop and walk the code
% block. i.e.,
% 1. Walk the block
% 2. Clean up the loop breaker
% 3. Remove the env
% 4. Decrement current Env
% 5. Proceed to next loop
% 'break' -> Stop the loop, and clean up. i.e.,
% 1. Clean up the loop breaker
% 2. Remove the env
% 3. Decrement the current env
% 4. Call the last iteration of the loop
case maybe_break() of
false ->
walk(Block),
removeEnv(Env + 1),
decrementEnv(),
for(Name, Tail, Block, Rest);
continue ->
walk(Block),
clearBreaker(),
removeEnv(Env + 1),
decrementEnv(),
for(Name, Tail, Block, Rest);
break ->
clearBreaker(),
removeEnv(Env + 1),
decrementEnv(),
for(Name, [], Block, Rest)
end.
% While loops
% Condition is not true, so continue walking the chain
while(false, _Condition, _Block) ->
decrementLoop(),
ok;
% Condition is currently true, so walk the Block and check for any breakers
while(true, Condition, Block) ->
incrementEnv(),
Env = getEnv(),
case maybe_break() of
false ->
walk(Block),
removeEnv(Env),
decrementEnv(),
while(expr(Condition), Condition, Block);
continue ->
walk(Block),
clearBreaker(),
removeEnv(Env),
decrementEnv(),
while(expr(Condition), Condition, Block);
break ->
clearBreaker(),
removeEnv(Env),
decrementEnv(),
while(false, Condition, Block)
end.
% Match statements
% Iterate through the list of match conditions, stopping when a match is
% successful.
match(_Expression, []) ->
ok;
match(_Expression, [{match_cond, {name, _L, "_"}, Block}]) ->
% Head is the last condition in the list and can match any expression
walk(Block);
match(Expression, [{match_cond, Condition, Block} | Tail]) ->
case match(expr(Expression), expr(Condition), Block) of
true ->
ok;
false ->
match(Expression, Tail)
end.
% If the condition is a success, then walk the block and return true so the
% match does not continue evaluating.
match(Val, Condition, Block) when Condition == Val ->
walk(Block),
true;
match(_Val, _Condition, _Block) ->
false.
% Enums are lists of constants with sequentially increasing value. Iterate through the list, assigning values
enum(List) ->
enum(List, 0).
enum([], _N) ->
ok;
enum([{name, _L, Name} | Tail], N) ->
declare(Name, N, var),
enum(Tail, N + 1).
%%============================================================================
%% Funs for working with the state tree
%%============================================================================
% Find an object of a given type in the current scope, recursively traversing
% upwards through the tree as necessary
get_obj(Type, Name) ->
State = erlang:get(state),
Env = State#state.curEnv,
get_obj(Type, Name, Env).
get_obj(Type, Name, 0) ->
State = erlang:get(state),
Envs = State#state.envs,
EnvMap = maps:get(0, Envs, #env{}),
Object =
case Type of
func ->
F = EnvMap#env.functions,
maps:get(Name, F, false);
var ->
V = EnvMap#env.variables,
maps:get(Name, V, false)
end,
{0, Object};
get_obj(Type, Name, EnvID) when EnvID > 0 ->
State = erlang:get(state),
Envs = State#state.envs,
EnvMap = maps:get(EnvID, Envs, #env{}),
case Type of
func ->
F = EnvMap#env.functions,
case maps:get(Name, F, false) of
false ->
get_obj(Type, Name, EnvID - 1);
Object ->
{EnvID, Object}
end;
var ->
V = EnvMap#env.variables,
case maps:get(Name, V, false) of
false ->
get_obj(Type, Name, EnvID - 1);
Object ->
{EnvID, Object}
end
end.
% Puts an object into the state table. Does not check scope rules, previous
% declarations, etc.
% UNUSED
%put_obj(Type, Name, Env) ->
% put_obj(Type, Name, 'Null', Env).
put_obj(Type, Name, Val, Env) ->
State = erlang:get(state),
Envs0 = State#state.envs,
EnvID = Env,
% Return or create map for Env given the EnvID
EnvMap0 = maps:get(EnvID, Envs0, #env{}),
EnvMap1 =
case Type of
func ->
F0 = EnvMap0#env.functions,
% Put the new function into the functions map
F1 = maps:put(Name, Val, F0),
EnvMap0#env{functions = F1};
var ->
V0 = EnvMap0#env.variables,
% Put the new variable into the variables map
V1 = maps:put(Name, Val, V0),
% Put the variables map into the Envs1
EnvMap0#env{variables = V1}
end,
% Put the new/updated environment into the Envs map
Envs1 = maps:put(EnvID, EnvMap1, Envs0),
% Update the state with the new environment map and return
State1 = State#state{envs = Envs1},
erlang:put(state, State1),
ok.
incrementLoop() ->
St0 = erlang:get(state),
Loop = St0#state.curLoop,
St1 = St0#state{curLoop = Loop + 1},
erlang:put(state, St1).
decrementLoop() ->
St0 = erlang:get(state),
Loop = St0#state.curLoop,
if
Loop > 0 ->
St1 = St0#state{curLoop = Loop - 1},
erlang:put(state, St1);
true ->
ok
end.
%UNUSED TODO
%getLoop() ->
% St0 = erlang:get(state),
% St0#state.curLoop.
removeEnv(Env) ->
St0 = erlang:get(state),
Envs0 = St0#state.envs,
St1 = St0#state{envs = maps:remove(Env, Envs0)},
erlang:put(state, St1).
incrementEnv() ->
St0 = erlang:get(state),
Env = St0#state.curEnv,
St1 = St0#state{curEnv = Env + 1},
erlang:put(state, St1).
decrementEnv() ->
St0 = erlang:get(state),
Env = St0#state.curEnv,
if
Env > 0 ->
St1 = St0#state{curEnv = Env - 1},
erlang:put(state, St1);
true ->
ok
end.
getEnv() ->
St0 = erlang:get(state),
St0#state.curEnv.
% TODO unused
%printState() ->
% St0 = erlang:get(state),
% io:format("Current state is: ~p~n", [St0]).
clearBreaker() ->
St0 = erlang:get(state),
Br0 = St0#state.breakers,
Loop = St0#state.curLoop,
Br1 = maps:remove(Loop, Br0),
erlang:put(state, St0#state{breakers = Br1}).
% Append any "print" statements or otherwise to the Console
console_append(Val) ->
console_append(Val, stdout).
console_append(Val, Channel) when Channel == 'stdout' ->
St0 = erlang:get(console),
Console0 = St0#console.stdout,
St1 = St0#console{stdout = [Val | Console0]},
erlang:put(console, St1);
console_append(Val, Channel) when Channel == 'stderr' ->
St0 = erlang:get(console),
Console0 = St0#console.stderr,
St1 = St0#console{stderr = [Val | Console0]},
erlang:put(console, St1).
console_get(Channel) ->
St0 = erlang:get(console),
case Channel of
stdout ->
lists:reverse(St0#console.stdout);
stderr ->
lists:reverse(St0#console.stderr)
end.
%% Print any messages
%console_print() ->
% console_print(stdout).
%
%console_print(Opt) when Opt == 'stdout' ->
% St0 = erlang:get(console),
% Console = St0#console.stdout,
% console_print(lists:reverse(Console));
%console_print(Opt) when Opt == 'stderr' ->
% St0 = erlang:get(console),
% Console = St0#console.stderr,
% console_print(lists:reverse(Console));
%console_print([]) ->
% ok;
%console_print([H|T]) ->
% io:format("~p~n", [H]),
% console_print(T).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Internal functions %
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
return(List) ->
return(List, maps:new()).
return([], Acc) ->
Acc;
return([Key | Rest], Acc0) ->
% Get the requested variable out of the State
Value = get_variable(Key),
Acc1 = maps:put(Key, Value, Acc0),
return(Rest, Acc1).
get_variable({name, _L, Var}) ->
get_variable(Var);
get_variable(Var) ->
{_, Val} = get_obj(var, Var),
Val.
% Provide two ways of using the '+' operator: adding numbers and concatinating
% strings.
overload_add(Val1, Val2) when is_number(Val1), is_number(Val2) ->
Val1 + Val2;
overload_add(Val1, Val2) when is_list(Val1), is_list(Val2) ->
Val1 ++ Val2.
maybe_break() ->
St0 = erlang:get(state),
% Get the current loop and breaker for this loop, if it exists.
Loop = St0#state.curLoop,
Breakers = St0#state.breakers,
% if no breaker exists, just return false
maps:get(Loop, Breakers, false).
application_function(Name, Args) ->
St0 = erlang:get(state),
AppFuns = St0#state.appFuns,
case maps:get(Name, AppFuns, undefined) of
undefined ->
undefined;
Fun ->
Fun(Args)
end.
local_function(Name, Args) ->
St0 = erlang:get(state),
Environment = St0#state.curEnv,
% Functions are only defined in the local scope (til we have Class support?)
Return =
case get_obj(func, Name, Environment) of
{_, false} ->
undefined;
{_, {Params, Block}} ->
local_function_block(Params, Block, Args)
end,
Return.
local_function_block(Params, Block, Args) when length(Params) == length(Args) ->
St0 = erlang:get(state),
Environment = St0#state.curEnv,
erlang:put(state, St0#state{curEnv = Environment + 1}),
% Setup the local variables in the environment, called for its side effects
setup_function_env(lists:zip(Args, Params)),
Ret = walk(Block),
% Remove the temporary environment and decrement the current state
removeEnv(Environment + 1),
St1 = erlang:get(state),
erlang:put(state, St1#state{curEnv = Environment}),
Ret;
local_function_block(_Params, _Block, _Args) ->
throw("Could not evaluate function: Wrong number of arguments").
setup_function_env([]) ->
ok;
setup_function_env([{Arg, Param} | Rest]) ->
{name, _, Name} = Param,
declare(Name, expr(Arg), func),
setup_function_env(Rest).
eval(List) when is_list(List) ->
eval(List, []);
eval(Expr) ->
expr(Expr).
eval([], Acc) ->
lists:reverse(Acc);
eval([H | T], Acc) ->
eval(T, [expr(H) | Acc]).
%%===========================================================================
%% Built-in functions callable from GDMinus
%%===========================================================================
builtin_function("print", []) ->
'Null';
builtin_function("print", [Head | Rest]) when is_map(Head) ->
% Closer to GDScript native formatting, but not quite. TODO replace ','
% with ':'
console_append(list_to_tuple(maps:to_list(Head))),
builtin_function("print", Rest);
builtin_function("print", [Head | Rest]) ->
console_append(Head),
builtin_function("print", Rest);
builtin_function("dict_has", [Dict,Arg]) ->
{_, V} = get_obj(var, Dict),
case maps:get(Arg, V, false) of
false -> false;
_ -> true
end;
builtin_function("dict_empty", [Dict]) ->
{_, V} = get_obj(var, Dict),
case maps:size(V) of
0 -> true;
_ -> false
end;
builtin_function("dict_size", [Dict]) ->
{_, V} = get_obj(var, Dict),
maps:size(V);
builtin_function("OS.get_ticks_msec", []) ->
erlang:system_time(millisecond);
builtin_function("OS.get_ticks_usec", []) ->
erlang:system_time(microsecond);
builtin_function("str", [Arg]) ->
str(Arg);
builtin_function("abs", [Arg]) ->
abs(Arg);
builtin_function("acos", [Arg]) ->
math:acos(Arg);
builtin_function("asin", [Arg]) ->
math:asin(Arg);
builtin_function("atan", [Arg]) ->
math:atan(Arg);
builtin_function("atan2", [X, Y]) ->
math:atan2(X, Y);
builtin_function("ceil", [Arg]) ->
math:ceil(Arg);
builtin_function("cos", [Arg]) ->
math:cos(Arg);
builtin_function("cosh", [Arg]) ->
math:cosh(Arg);
builtin_function("exp", [Arg]) ->
math:exp(Arg);
builtin_function("floor", [Arg]) ->
math:floor(Arg);
builtin_function("fmod", [X, Y]) ->
math:fmod(X, Y);
builtin_function("log", [Arg]) ->
math:log(Arg);
builtin_function("max", [X, Y]) ->
erlang:max(X, Y);
builtin_function("min", [X, Y]) ->
erlang:min(X, Y);
builtin_function("pow", [X, Y]) ->
math:pow(X, Y);
builtin_function("randf", []) ->
rand:uniform();
builtin_function("randi", []) ->
rand:uniform(trunc(math:pow(2, 32) - 1));
builtin_function("randomize", []) ->
rand:seed(exs1024s),
'Null';
builtin_function("range", [Arg]) ->
lists:seq(1, trunc(Arg));
builtin_function("range", [Arg1, Arg2]) ->
lists:seq(trunc(Arg1), trunc(Arg2));
builtin_function("round", [Arg]) ->
erlang:round(Arg);
builtin_function("sin", [Arg]) ->
math:sin(Arg);
builtin_function("sinh", [Arg]) ->
math:sinh(Arg);
builtin_function("sqrt", [Arg]) ->
math:sqrt(Arg);
builtin_function("tan", [Arg]) ->
math:tan(Arg);
builtin_function("tanh", [Arg]) ->
math:tanh(Arg);
builtin_function(Func, _Args) ->
% Not a built-in function, and presumably the local function call also failed.
Reason = io_lib:format("Function not defined or not implemented:", Func),
throw(Reason).
str(Arg) when is_integer(Arg) ->
integer_to_list(Arg);
str(Arg) when is_list(Arg) ->
Arg;
str(Arg) when is_float(Arg) ->
float_to_list(Arg).