%% @doc Guard in
%% @author Michael Uvarov (freeakk@gmail.com)
%% Source: https://github.com/mad-cocktail/gin
%% License: MIT
-module(gin).
-author('freeakk@gmail.com').
-export([parse_transform/2]).
parse_transform(Forms, _Options) ->
F1 = local_function(numeric_in, 2, in_transform('==')),
F2 = local_function(in, 2, in_transform('=:=')),
F3 = local_function(beetween, 3, fun beetween_transform/1),
F = foldl_functions([F1, F2, F3, fun erl_syntax:revert/1]),
X = [erl_syntax_lib:map(F, Tree) || Tree <- Forms],
% io:format(user, "Before:\t~p\n\nAfter:\t~p\n", [Forms, X]),
X.
%% ==================================================================
%% In
%% ==================================================================
%% It is curry (from Haskell) for `in_transform/2'.
in_transform(Op) ->
fun(Node) ->
in_transform(Op, Node)
end.
%% @doc Replace `in(X, List)' with `(X =:= E1) andalso (X =:= E2)'
%% when `List' is `[E1, E2]' and `Op' is `=:='.
%%
%% The caller checks, that the function name is valid.
%% `in' can be any function, for example, `in2' is valid too.
-spec in_transform(Op, Node) -> Node when
Op :: '==' | '=:=',
Node :: erl_syntax_lib:syntaxTree().
in_transform(Op, Node) ->
Pos = erl_syntax:get_pos(Node),
%% Call it fore all new nodes.
New = fun(NewNode) -> erl_syntax:set_pos(NewNode, Pos) end,
%% Extract arguments of the `in' function.
[SubjectForm, ListForm] = erl_syntax:application_arguments(Node),
Elems =
case erl_syntax:type(ListForm) of
string ->
Str = erl_syntax:string_value(ListForm),
[erl_syntax:char(C) || C <- Str];
list ->
%% Extract the list of the valid values.
erl_syntax:list_elements(ListForm)
end,
case Elems of
[] ->
%% Always `false'.
New(erl_syntax:atom(false));
_ ->
EqOp = New(erl_syntax:operator(Op)),
OrOp = New(erl_syntax:operator('orelse')),
%% `X' is `Subject =:= Xs'.
[X|Xs] = [New(erl_syntax:infix_expr(E, EqOp, SubjectForm)) || E <- Elems],
F = fun(Right, Left) -> New(erl_syntax:infix_expr(Left, OrOp, Right)) end,
GuardAST = New(erl_syntax:parentheses(lists:foldl(F, X, Xs))),
erl_syntax:revert(GuardAST)
end.
%% ==================================================================
%% Beetween
%% ==================================================================
%% @doc Transforms `beetween(Subject, Start, To)'.
%% Subject is a term, but usually it is a number.
%% `From' and `To' can be wrapped with the `open(_)' call.
%% It meand, that this value is not inluded in the interval.
%%
%% `beetween(X, F, T)' is replaced with `((X =< F) andalso (X >= T))'.
%% `beetween(X, open(F), T)' is replaced with `((X < F) andalso (X >= T))'.
beetween_transform(Node) ->
Pos = erl_syntax:get_pos(Node),
%% Call it fore all new nodes.
New = fun(NewNode) -> erl_syntax:set_pos(NewNode, Pos) end,
%% Extract arguments of the `in' function.
[SubjectForm, FromForm, ToForm] =
erl_syntax:application_arguments(Node),
GtEqOp = New(erl_syntax:operator(greater(is_open(FromForm)))),
LoEqOp = New(erl_syntax:operator(less(is_open(ToForm)))),
AndOp = New(erl_syntax:operator('andalso')),
Exp1 = New(erl_syntax:infix_expr(SubjectForm, GtEqOp, clean_open(FromForm))),
Exp2 = New(erl_syntax:infix_expr(SubjectForm, LoEqOp, clean_open(ToForm))),
Exp3 = New(erl_syntax:infix_expr(Exp1, AndOp, Exp2)),
GuardAST = New(erl_syntax:parentheses(Exp3)),
erl_syntax:revert(GuardAST).
%% @doc Returns an operator name.
-spec less(IsExcluded) -> Op when
IsExcluded :: boolean(),
Op :: atom().
less(true) -> '<';
less(false) -> '=<'.
-spec greater(IsExcluded) -> Op when
IsExcluded :: boolean(),
Op :: atom().
greater(true) -> '>';
greater(false) -> '>='.
%% @doc Return true, if `Node' is wrapped by `open(_)'.
is_open(Node) ->
is_local_function(open, 1, Node).
%% @doc Convert the call of `open(Body)' to `Body'.
clean_open(Node) ->
case is_open(Node) of
true -> hd(erl_syntax:application_arguments(Node));
false -> Node
end.
foldl_functions(Fs) ->
fun(Node) ->
Apply = fun(F, N) -> F(N) end,
lists:foldl(Apply, Node, Fs)
end.
local_function(FunName, FunArity, TransFun) ->
fun(Node) ->
IsFun = is_local_function(FunName, FunArity, Node),
if IsFun -> TransFun(Node);
true -> Node
end
end.
%% @doc Return `true', `Node' is a function call of the `FunName/FunArity' function.
is_local_function(FunName, FunArity, Node) ->
erl_syntax:type(Node) =:= application
andalso always(Op = erl_syntax:application_operator(Node))
andalso erl_syntax:type(Op) =:= atom
andalso erl_syntax:atom_value(Op) =:= FunName
andalso application_arity(Node) =:= FunArity.
always(_) -> true.
%% @doc Return arity of the called function inside `Node'.
application_arity(Node) ->
length(erl_syntax:application_arguments(Node)).