src/defarg.erl

%%% vim:ts=2:sw=2:et
%%%-----------------------------------------------------------------------------
%%% @doc Erlang parse transform for permitting default arguments in functions
%%%
%%% Presently the Erlang syntax doesn't allow function arguments to have default
%%% parameters.  Consequently a developer needs to replicate the function
%%% definition multiple times passing constant defaults to some parameters of
%%% functions.
%%%
%%% This parse transform addresses this shortcoming by extending the syntax
%%% of function definitions at the top level in a module to have a default
%%% expression such that for `A / Default' argument the `Default' will be
%%% used if the function is called in code without that argument.
%%%
%%% ```
%%% -export([t/2]).
%%%
%%% test(A / 10, B / 20) ->
%%%   A + B.
%%% '''
%%% The code above is transformed to:
%%% ```
%%% -export([t/2]).
%%% -export([t/0, t/1]).
%%%
%%% test()    -> test(10);
%%% test(A)   -> test(A, 20);
%%% test(A,B) -> A+B.
%%% '''
%%%
%%% The arguments with default values must be at the end of the argument list:
%%% ```
%%% test(A, B, C / 1) ->    %% This is valid
%%%   ...
%%%
%%% test(A / 1, B, C) ->    %% This is invalid
%%%   ...
%%% '''
%%%
%%% Default arguments must be constants or arithmetic expressions.  Function
%%% calls are not supported as default arguments due to the limitations of the
%%% Erlang parser.
%%%
%%% @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(defarg).

-export([parse_transform/2]).

%% @doc parse_transform entry point
parse_transform(AST, Options) ->
  etran_util:process(?MODULE,
                     fun(Ast) -> replace(Ast) end,
                     AST, Options).

replace(AST) ->
  ModExports = lists:sort(lists:append([Exp || {attribute, _, export, Exp} <- AST])),
  replace(AST, undefined, [], ModExports, []).

replace([], _Mod, Exports, _ModExports, Acc) ->
  Res = lists:reverse(Acc),
  {HeadAST, [{attribute, Loc, _, _} = ModAST|TailAST]} =
    lists:splitwith(fun({attribute, _, module, _}) -> false; (_) -> true end, Res),
  AddExports = [{attribute, Loc, export, Exp} || Exp <- lists:reverse(Exports)],
  HeadAST ++ [ModAST] ++ AddExports ++ TailAST;

replace([{attribute,_,module,Mod}=H|T], _, Exports, ModExports, Acc) ->
  replace(T, Mod, Exports, ModExports, [H|Acc]);

replace([{function, Loc, Fun, Arity, [{clause, CLoc, Args, Guards, Body}]}=H|T],
        Mod, Exports, ModExports, Acc) ->
  {RevDef, RevRestArgs} =
    lists:splitwith(
      fun({op, _, '/', _Arg, _Def}) -> true;
         (_)                        -> false
      end,
      lists:reverse(Args)),
  {FrontArgs, DefArgs} =
    {lists:reverse(RevRestArgs), lists:reverse([{A,D} || {op, _, '/', A, D} <- RevDef])},

  case DefArgs of
    [] ->
      replace(T, Mod, Exports, ModExports, [H|Acc]);
    _ ->
      lists:filter(fun({op, _, '/', _A, _D}) -> true; (_) -> false end, FrontArgs) /= []
        andalso throw(lists:flatten(
                        lists:format(
                          "Function ~w:~w/~w has default arguments not at the end of the argument list!",
                          [get(key), Fun, Arity]))),
      %% Add new exports, e.g.: -export([f/2]).
      N = Arity - length(DefArgs),
      NewExports = case lists:member({Fun,Arity}, ModExports) of
                     true  -> [[{Fun,I} || I <- lists:seq(N, Arity-1)] | Exports];
                     false -> Exports
                   end,

      LastClause = {function, Loc, Fun, Arity,
                     [{clause, CLoc, FrontArgs ++ [A || {A,_} <- DefArgs], Guards, Body}]},

      AddClauses = element(3,
                    lists:foldl(fun({A, D}, {Front, ArityN, Acc1}) ->
                      Acc2 = [{function, Loc, Fun, ArityN,
                               [{clause, CLoc, Front, [],
                                 [{call, CLoc, {atom, CLoc, Fun}, Front ++ [D]}]}]} | Acc1],
                      {Front ++ [A], ArityN+1, Acc2}
                    end, {FrontArgs, N, []}, DefArgs)),

      replace(T, Mod, NewExports, ModExports, [LastClause | AddClauses] ++ Acc)
  end;

replace([H|T], Mod, Exports, ModExports, Acc) ->
  replace(T, Mod, Exports, ModExports, [H|Acc]).