src/etran_util.erl

%%%-----------------------------------------------------------------------------
%%% @doc    Erlang parse transform utility functions
%%% @author Serge Aleynikov <saleyn(at)gmail(dot)com>
%%% @end
%%%-----------------------------------------------------------------------------
%%% Copyright (c) 2021 Serge Aleynikov
%%%
%%% Permission is hereby granted, free of charge, to any person
%%% obtaining a copy of this software and associated documentation
%%% files (the "Software"), to deal in the Software without restriction,
%%% including without limitation the rights to use, copy, modify, merge,
%%% publish, distribute, sublicense, and/or sell copies of the Software,
%%% and to permit persons to whom the Software is furnished to do
%%% so, subject to the following conditions:
%%%
%%% The above copyright notice and this permission notice shall be included
%%% in all copies or substantial portions of the Software.
%%%
%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
%%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
%%% MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
%%% IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
%%% CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
%%% TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
%%% SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
%%%-----------------------------------------------------------------------------
-module(etran_util).
-export([transform/2, transform/3, apply_transform/4, process/4]).
-export([parse_options/2, debug_options/2, source_forms/2]).

%%------------------------------------------------------------------------------
%% @doc Transform `Forms' by applying a lambda `Fun'.
%% @end
%%------------------------------------------------------------------------------
-spec transform(fun((Forms::term()) -> tuple()|continue), Forms::term()) -> list().
transform(Fun, Forms) when is_function(Fun, 1), is_list(Forms) ->
  transform2(Fun, Forms);
transform(Fun, Form) when is_function(Fun, 1), is_tuple(Form) ->
  transform2(Fun, [Form]).

transform2(_, []) ->
  [];
transform2(Fun, [L|Fs]) when is_list(L) ->
  [transform2(Fun, L) | transform2(Fun, Fs)];
transform2(Fun, [F|Fs]) when is_tuple(F), is_atom(element(1,F)) ->
  case Fun(F) of
    NewF when is_tuple(NewF) ->
      [NewF | transform2(Fun, Fs)];
    continue ->
      [list_to_tuple(transform2(Fun, tuple_to_list(F))) | transform2(Fun, Fs)]
  end;
transform2(Fun, [F|Fs]) ->
  [F | transform2(Fun, Fs)];
transform2(_, F) ->
  F.

%%------------------------------------------------------------------------------
%% @doc Transform `Forms' by applying a lambda `Fun'.
%% @end
%%------------------------------------------------------------------------------
-spec transform(fun((Forms::term(), State::term()) ->
                    {tuple()|continue, NewState::term()}),
                Forms::term(), State::term()) ->
        {list(), NewState::term()}.
transform(Fun, Forms, State) when is_function(Fun, 2), is_list(Forms) ->
  transform2(Fun, Forms, State).

transform2(_, [], State) ->
  {[], State};
transform2(Fun, [L|Fs], State) when is_list(L) ->
  {Res1, ResSt1} = transform2(Fun, L,  State),
  {Res2, ResSt2} = transform2(Fun, Fs, ResSt1),
  {[Res1 | Res2], ResSt2};
transform2(Fun, [F|Fs], State) when is_tuple(F), is_atom(element(1,F)) ->
  case Fun(F, State) of
    {NewF, NewS} when is_tuple(NewF) ->
      {Res, ResSt} = transform2(Fun, Fs, NewS),
      {[NewF | Res], ResSt};
    {continue, ResSt} ->
      {Res1, ResSt1} = transform2(Fun, tuple_to_list(F), ResSt),
      {Res2, ResSt2} = transform2(Fun, Fs, ResSt1),
      {[list_to_tuple(Res1) | Res2], ResSt2}
  end;
transform2(Fun, [F|Fs], State) ->
  {Res, ResSt} = transform2(Fun, Fs, State),
  {[F | Res], ResSt};
transform2(_, F, State) ->
  {F, State}.

%%------------------------------------------------------------------------------
%% @doc Apply parse transform with debug printing options
%% @end
%%------------------------------------------------------------------------------
apply_transform(Module, Fun, AST, Options) when is_atom(Module)
                                              , is_function(Fun, 1)
                                              , is_list(Options) ->
  process(Module, fun(Ast) -> transform(Fun, Ast) end, AST, Options).

%%------------------------------------------------------------------------------
%% @doc Call `Fun' for the AST and optionally print debug info
%% @end
%%------------------------------------------------------------------------------
process(Module, Fun, AST, Options) when is_atom(Module)
                                      , is_function(Fun, 1)
                                      , is_list(Options) ->
  #{orig := OrigAST, ast := ResAST, src := SrcAST} =
    debug_options(Module, Options),

  OrigAST andalso io:format(">>> Before ~s:\n  ~p~n", [Module, AST]),
  Transformed   = Fun(AST),
  ResAST  andalso io:format(">>> After ~s:  ~p~n", [Module, Transformed]),
  SrcAST  andalso source_forms(Transformed,
                    [print, {format, ">>> Resulting Source:\n  ~s~n"}]),
  Transformed.

%%------------------------------------------------------------------------------
%% @doc Check if `KnownFlags' are found in Options.
%% @end
%%------------------------------------------------------------------------------
-spec parse_options(list(), list()) -> [boolean()].
parse_options(KnownFlags, Options) when is_list(KnownFlags), is_list(Options) ->
  [case lists:keyfind(I, 2, Options) of
     {d, I, Val} -> Val;
     {d, I}      -> true;
     false       -> false
   end || I <- KnownFlags].

%%------------------------------------------------------------------------------
%% @doc Get parse transforms debug options
%% @end
%%------------------------------------------------------------------------------
-spec debug_options(atom(), list()) ->
        #{orig => boolean(), ast => boolean(), src => boolean()}.
debug_options(Module, Options) when is_atom(Module), is_list(Options) ->
  M = atom_to_list(Module),
  DbgOrig = list_to_atom(M ++ "_orig"),
  DbgAST  = list_to_atom(M ++ "_ast"),
  DbgSrc  = list_to_atom(M ++ "_src"),
  [OrigAST, ResAST, SrcAST] =
    parse_options([DbgOrig, DbgAST, DbgSrc], Options),
  #{orig => OrigAST, ast => ResAST, src => SrcAST}.

%%------------------------------------------------------------------------------
%% @doc Decompile source code from the AST
%% @end
%%------------------------------------------------------------------------------
-spec source_forms(list(), list()) -> ok | string().
source_forms(AST, Options) ->
  Res = erl_prettypr:format(erl_syntax:form_list(tl(AST))),
  case lists:member(print, Options) of
    true ->
      io:format(proplists:get_value(format, Options, "~s\n"), [Res]);
    false ->
      Res
  end.