%%% ============================================================== [ smerl.erl ]
%%% @doc Simple Metaprogramming for Erlang
%%% @author Yariv Sadan
%%% @copyright 2006-2007, 2016 AUTHORS
%%%
%%% Smerl is an Erlang library
%%% that simplifies the creation and manipulation of Erlang modules in
%%% runtime.
%%%
%%% You don't need to know Smerl in order to use ErlyWeb; Smerl
%%% is included in ErlyWeb because ErlyWeb uses it internally.
%%%
%%% Smerl uses Erlang's capabilities for hot code swapping and
%%% abstract syntax tree transformations to do its magic. Smerl is inspired by
%%% the rdbms_codegen.erl module in the RDBMS application written by
%%% Ulf Wiger. RDBMS is part of Jungerl ([http://jungerl.sf.net]).
%%%
%%% Here's a quick example illustrating how to use Smerl:
%%% ```
%%% test_smerl() ->
%%% M1 = smerl:new(foo),
%%% {ok, M2} = smerl:add_func(M1, "bar() -> 1 + 1."),
%%% smerl:compile(M2),
%%% foo:bar(), % returns 2``
%%% smerl:has_func(M2, bar, 0). % returns true
%%% '''
%%%
%%% New functions can be expressed either as strings of Erlang code
%%% or as abstract forms. For more information, read the Abstract Format
%%% section in the ERTS User's guide
%%% ([http://erlang.org/doc/doc-5.5/erts-5.5/doc/html/absform.html#4]).
%%%
%%% Using the abstract format, the 3rd line of the above example
%%% would be written as
%%% ```
%%% {ok,M2} = smerl:add_func(M1, {function,1,bar,0,
%%% [{clause,1,[],[],
%%% [{op,1,'+',{integer,1,1},{integer,1,1}}]}]).
%%% '''
%%%
%%% <p>The abstact format may look more verbose in this example, but
%%% it's also easier to manipulate in code.</p>
%%% @end
%%% 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.
%%% ==================================================================== [ EOH ]
-module(smerl).
-author("Yariv Sadan (yarivsblog@gmail.com, http://yarivsblog.com").
%% Public API.
-export([new/1,
for_module/1, for_module/2, for_module/3,
for_file/1, for_file/2, for_file/3,
get_module/1, set_module/2,
get_forms/1, set_forms/2,
get_exports/1, set_exports/2, get_export_all/1, set_export_all/2,
remove_export/3,
get_attribute/2,
add_func/2, add_func/3, remove_func/3, has_func/3, get_func/3,
replace_func/2, % replace_func/3,
compile/1, compile/2,
rename/2,
curry/2, curry/4, curry/5,
curry_add/3, curry_add/4, curry_add/5, curry_add/6,
curry_replace/3, curry_replace/4,
embed_args/2, embed_args/4, embed_args/5, embed_all/2,
extend/2, extend/3, extend/4,
to_src/1, to_src/2
]).
%%% ================================================================== [ Types ]
-export_type([args/0, export/0, exports/0,
func_form/0, func_forms/0,
meta_mod/0,
result/1, result/2,
ok_t/1, error_t/1]).
%% TODO: write docstring
-type args() :: term() | [term()].
%% TODO: write docstring
-type export() :: {Function :: atom(), Arity :: arity()}.
-type exports() :: [export()].
%% A list of {@type export()}s.
-type func_form() :: erl_parse:abstract_form().
%% The abstract form for the function, as described
%% in the ERTS Users' manual.
-type func_forms() :: [func_form()].
%% A list of {@type func_form()}s.
%% The record type holding the abstract representation for a module.
-record(meta_mod, {module :: module(),
file :: undefined | file:filename(),
exports = [] :: exports(),
forms = [] :: func_forms(),
export_all = false :: boolean()
}).
-type meta_mod() :: #meta_mod{}.
%% A data structure holding the abstract representation
%% for a module.
-type result(Value) :: result(Value, term()).
-type result(Value, Error) :: {ok, Value} | {error, Error}.
-type ok_t(Value) :: {ok, Value} | error.
-type error_t(Error) :: ok | {error, Error}.
%%% ================================================================= [ Macros ]
-define(IF(Test, Then, Else), case Test of true -> Then; false -> Else end).
%%% ============================================================= [ Public API ]
-include_lib("kernel/include/file.hrl").
%% @doc Create a new meta_mod for a module with the given name.
-spec new(Module :: module()) -> meta_mod().
new(ModuleName) when is_atom(ModuleName) ->
#meta_mod{module = ModuleName}.
%% @equiv for_module(ModuleName, [])
for_module(ModuleName) ->
for_module(ModuleName, []).
%% @equiv for_module(ModuleName, IncludePaths, [])
for_module(ModuleName, IncludePaths) ->
for_module(ModuleName, IncludePaths, []).
%% @doc Create a meta_mod tuple for an existing module. If ModuleName is a
%% string, it is interpreted as a file name (this is the same as calling
%% {@link for_file/3}). If ModuleName is an atom, <em>Smerl</em> attempts to
%% find its abstract represtation either from its source file or from
%% its .beam file directly (if it has been compiled with debug_info).
%% If the abstract representation can't be found, this function returns
%% an error.
%%
%% The `IncludePaths' argument is used when `ModuleName' is a file name.
-spec for_module(ModuleName, IncludePaths, Macros) -> result(meta_mod) when
ModuleName :: atom() | string(),
IncludePaths :: [string()],
Macros :: [{atom(), term()}].
for_module(ModuleName, IncludePaths, Macros) when is_list(ModuleName) ->
for_file(ModuleName, IncludePaths, Macros);
for_module(ModuleName, IncludePaths, Macros) when is_atom(ModuleName) ->
[_Exports, _Imports, _Attributes,
{compile, [_Options, _Version, _Time, {source, Path}]}] =
ModuleName:module_info(),
case for_file(Path, IncludePaths, Macros) of
{ok, _Mod} = Res -> Res;
_Error -> get_module_forms(ModuleName)
end.
%% @equiv for_file(SrcFilePath, [])
for_file(SrcFilePath) ->
for_file(SrcFilePath, []).
%% @equiv for_file(SrcFilePath, IncludePaths, [])
for_file(SrcFilePath, IncludePaths) ->
for_file(SrcFilePath, IncludePaths, []).
%% @doc Create a meta_mod for a module from its source file.
-spec for_file(SrcFilePath, IncludePaths, Macros) -> Result when
SrcFilePath :: file:filename(),
IncludePaths :: [file:filename()],
Macros :: [{module(), atom()}],
Result :: result(meta_mod(), invalid_module).
for_file(SrcFilePath, IncludePaths, Macros) ->
case epp:parse_file(SrcFilePath, [filename:dirname(SrcFilePath) |
IncludePaths], Macros) of
{ok, Forms} ->
mod_for_forms(Forms);
_Err ->
{error, {invalid_module, SrcFilePath}}
end.
%% @doc Return the module name for the meta_mod.
-spec get_module(MetaMod :: meta_mod()) -> module().
get_module(MetaMod) ->
MetaMod#meta_mod.module.
%% @doc Set the meta_mod's module name.
-spec set_module(MetaMod, NewName) -> NewMod when
MetaMod :: meta_mod(),
NewName :: module(),
NewMod :: meta_mod().
set_module(MetaMod, NewName) ->
MetaMod#meta_mod{module = NewName}.
%% @doc Return the list of function forms in the meta_mod.
-spec get_forms(MetaMod :: meta_mod()) -> func_forms().
get_forms(MetaMod) ->
MetaMod#meta_mod.forms.
-spec set_forms(MetaMod, Forms) -> NewMod when
MetaMod :: meta_mod(),
Forms :: func_forms(),
NewMod :: meta_mod().
set_forms(MetaMod, Forms) ->
MetaMod#meta_mod{forms = Forms}.
%% @doc Return the list of exports in the meta_mod.
-spec get_exports(MetaMod :: meta_mod()) -> exports().
get_exports(MetaMod) ->
?IF(not MetaMod#meta_mod.export_all, MetaMod#meta_mod.exports,
lists:foldl(
fun({function, _L, Name, Arity, _Clauses}, Exports) ->
[{Name, Arity} | Exports];
(_Form, Exports) ->
Exports
end, [], MetaMod#meta_mod.forms)).
%% @doc Set the `MetaMod''s export list to `Exports'.
-spec set_exports(MetaMod, Exports) -> NewMod when
MetaMod :: meta_mod(),
Exports :: exports(),
NewMod :: meta_mod().
set_exports(MetaMod, Exports) ->
MetaMod#meta_mod{exports = Exports}.
%% @doc Get the `export_all' value for `MetaMod'.
-spec get_export_all(MetaMod :: meta_mod()) -> boolean().
get_export_all(MetaMod) ->
MetaMod#meta_mod.export_all.
%% @doc Set the `export_all' value for `MetaMod'.
-spec set_export_all(MetaMod, Value) -> NewMod when
MetaMod :: meta_mod(),
Value :: boolean(),
NewMod :: meta_mod().
set_export_all(MetaMod, Val) ->
MetaMod#meta_mod{export_all = Val}.
%% @doc Remove an export `{Function, Arity}'
%% from the list of `exports' in `MetaMod'.
-spec remove_export(MetaMod, Function, Arity) -> NewMod when
MetaMod :: meta_mod(),
Function :: atom(),
Arity :: arity(),
NewMod :: meta_mod().
remove_export(MetaMod, FuncName, Arity) ->
MetaMod#meta_mod{exports =
lists:delete({FuncName, Arity},
MetaMod#meta_mod.exports)}.
%% @doc Get the value of `MetaMod''s `Key' attribute.
-spec get_attribute(MetaMod :: meta_mod(), Key :: atom()) -> result(term()).
get_attribute(MetaMod, Key) ->
case lists:keyfind(Key, 3, get_forms(MetaMod)) of
{attribute, _, _, Val} -> {ok, Val};
_ -> error
end.
%% @doc Add a new exported function to `MetaMod'.
%% @equiv add_func(MetaMod, Form, true)
-spec add_func(MetaMod, Form) -> result(meta_mod(), parse_error) when
MetaMod :: meta_mod(),
Form :: func_form() | string().
add_func(MetaMod, Form) ->
add_func(MetaMod, Form, true).
%% @doc Add `Function' to `MetaMod' and return the new {@type meta_mod()}. If
%% `Export' is `true', add `Function' to `MetaMod''s `exports'.
-spec add_func(MetaMod, Func, Export) -> result(meta_mod(), parse_error) when
MetaMod :: meta_mod(),
Func :: func_form() | string(),
Export :: boolean().
add_func(MetaMod, Func, Export) when is_list(Func) ->
case parse_func_string(Func) of
{ok, Form} ->
add_func(MetaMod, Form, Export);
Err ->
Err
end;
add_func(MetaMod, {function, _Line, FuncName, Arity, _Clauses} = Form, true) ->
Foo = {ok, MetaMod#meta_mod{
exports = [{FuncName, Arity} | MetaMod#meta_mod.exports],
forms = [Form | MetaMod#meta_mod.forms]
}},
Foo;
add_func(MetaMod, {function, _Line, _Func, _Arity, _Clauses} = Form, false) ->
{ok, MetaMod#meta_mod{forms = [Form | MetaMod#meta_mod.forms]}};
add_func(_, _, _) ->
{error, parse_error}.
%% @doc Try to remove `Function' from `MetaMod'.
%% If the function exists, return the new {@type meta_mod()}.
%% Otherwise, return `MetaMod'.
-spec remove_func(MetaMod, Function, Arity) -> NewMod when
MetaMod :: meta_mod(),
Function :: atom(),
Arity :: arity(),
NewMod :: meta_mod().
remove_func(MetaMod, Function, Arity) ->
Forms = [ Form || {function, _L, F, A, _Cs} = Form <- MetaMod#meta_mod.forms,
F =:= Function, A =:= Arity ],
Exports = [ {F, A} || {F, A} <- MetaMod#meta_mod.exports,
F =:= Function, A =:= Arity ],
MetaMod#meta_mod{forms = Forms, exports = Exports}.
%% @doc Check whether `MetaMod' has a function `Function'/`Arity'.
-spec has_func(MetaMod, Function, Arity) -> boolean() when
MetaMod :: meta_mod(),
Function :: atom(),
Arity :: arity().
has_func(MetaMod, FuncName, Arity) ->
lists:any(fun({function, _Line, FuncName1, Arity1, _Clauses})
when FuncName1 == FuncName, Arity1 == Arity ->
true;
(_) ->
false
end, MetaMod#meta_mod.forms).
%% @doc Attempt to get the {@type func_form()} for `MetaMod':`Function'/`Arity'.
-spec get_func(MetaMod, Function, Arity) -> result(func_form()) when
MetaMod :: meta_mod() | module(),
Function :: atom(),
Arity :: arity().
get_func(Module, FuncName, Arity) when is_atom(Module) ->
case smerl:for_module(Module) of
{ok, C1} ->
get_func(C1, FuncName, Arity);
Err ->
Err
end;
get_func(MetaMod, FuncName, Arity) ->
do_get_func(MetaMod#meta_mod.forms, FuncName, Arity).
%% @doc Replace an existing function with a new one. If a matching function
%% doesn't exist, add `Function' to `MetaMod'. This is tantamount to calling
%% {@link remove_func/3} followed by {@link add_func/2}.
-spec replace_func(MetaMod, Function) -> result(meta_mod()) when
MetaMod :: meta_mod(),
Function :: string() | func_form().
replace_func(MetaMod, Function) when is_list(Function) ->
case parse_func_string(Function) of
{ok, Form} ->
replace_func(MetaMod, Form);
Err ->
Err
end;
replace_func(MetaMod, {function, _Line, FuncName, Arity, _Clauses} = Form) ->
Mod1 = remove_func(MetaMod, FuncName, Arity),
add_func(Mod1, Form);
replace_func(_MetaMod, _) ->
{error, parse_error}.
%% @doc Compile `MetaMod' and load the resulting BEAM into the emulator.
%% @equiv compile(MetaMod, [])
-spec compile(MetaMod :: meta_mod()) -> error_t(term()).
compile(MetaMod) ->
compile(MetaMod, []).
%% @doc Compile `MetaMod' and load the resulting BEAM into the emulator.
%% `Options' is a list of options as described in the `compile' module in the
%% Erlang documentation.
%% If an `outdir' is provided, write the `.beam' file to it.
%% @equiv compile(MetaMod, [report_errprs, report_warnings, return_errors])
-spec compile(MetaMod, Options) -> error_t(term()) when
MetaMod :: meta_mod(),
Options :: [proplists:property()].
compile(MetaMod, []) ->
compile(MetaMod, [report_errors, report_warnings,
return_errors]);
compile(MetaMod, Options) ->
Forms = [{attribute, 2, module, MetaMod#meta_mod.module},
{attribute, 3, export, get_exports(MetaMod)}],
FileName = case MetaMod#meta_mod.file of
undefined -> atom_to_list(get_module(MetaMod));
Val -> Val
end,
Forms1 = [{attribute, 1, file, {FileName, 1}} | Forms],
Forms2 = Forms1 ++ lists:reverse(MetaMod#meta_mod.forms),
compile(MetaMod#meta_mod.module, Forms2, Options).
%% @doc Change the name of the function represented by `Form' to `NewName'.
-spec rename(Form :: func_form(), NewName :: atom()) -> func_form().
rename({function, Line, _Name, Arity, Clauses}, NewName) ->
{function, Line, NewName, Arity, Clauses}.
%% @doc Get the curried form for `Form' with `Args'.
%% Here, "currying" involves replacing one or more of the function's leading
%% arguments with predefined values.
-spec curry(Form :: func_form(), Args :: args()) -> result(func_form()).
curry(Form, Arg) when not is_list(Arg) ->
curry(Form, [Arg]);
curry({function, _Line, _Name, Arity, _Clauses}, Args)
when length(Args) > Arity ->
{error, too_many_args};
curry({function, Line, Name, Arity, Clauses}, NewArgs) ->
NewClauses =
lists:foldl(
fun(Clause, Clauses1) ->
[curry_clause(Clause, NewArgs) | Clauses1]
end, [], Clauses),
{ok, {function, Line, Name, Arity-length(NewArgs), NewClauses}}.
%% @doc Curry `Module':`Function'/`Arity' with the given `Args'.
-spec curry(Module, Function, Arity, Args) -> result(func_form()) when
Module :: module() | meta_mod(),
Function :: atom(),
Arity :: arity(),
Args :: args().
curry(ModName, Name, Arity, Args) when is_atom(ModName) ->
case for_module(ModName) of
{ok, MetaMod} ->
curry(MetaMod, Name, Arity, Args);
Err ->
Err
end;
curry(MetaMod, Name, Arity, Args) ->
case get_func(MetaMod, Name, Arity) of
{ok, Form} ->
curry(Form, Args);
Err ->
Err
end.
%% @doc Curry `Module':`Function'/`Arity'with the given `Args',
%% renaming it to `NewName' and return the renamed form.
-spec curry(Module, Function, Arity, Args, NewName) -> result(func_form()) when
Module :: module() | meta_mod(),
Function :: atom(),
Arity :: arity(),
Args :: args(),
NewName :: atom().
curry(Module, Name, Arity, Args, NewName) ->
case curry(Module, Name, Arity, Args) of
{ok, NewForm} ->
{ok, rename(NewForm, NewName)};
Err ->
Err
end.
%% @doc Add `Form' curried with `Args' to `MetaMod'.
-spec curry_add(MetaMod, Form, Args) -> result(meta_mod()) when
MetaMod :: meta_mod(),
Form :: func_form(),
Args :: args().
curry_add(MetaMod, {function, _Line, Name, Arity, _Clauses}, Args) ->
curry_add(MetaMod, Name, Arity, Args).
%% @doc Add `Function'/`Arity' curried with `Args' to `MetaMod'.
-spec curry_add(MetaMod, Function, Arity, Args) -> result(meta_mod()) when
MetaMod :: meta_mod(),
Function :: atom(),
Arity :: arity(),
Args :: args().
curry_add(MetaMod, Name, Arity, Args) ->
curry_change(MetaMod, Name, Arity, Args, false).
%% @doc Curry `MetaMod':`Function'/`Arity' and add it to `MetaMod' as `NewName'.
-spec curry_add(MetaMod, Function, Arity, Args, NewName) -> Result when
MetaMod :: meta_mod(),
Function :: atom(),
Arity :: arity(),
Args :: args(),
NewName :: atom(),
Result :: result(meta_mod(), parse_error).
curry_add(MetaMod, Name, Arity, Args, NewName) ->
curry_add(MetaMod, MetaMod, Name, Arity, Args, NewName).
%% @doc Curry `Module':`Function'/`Arity' and add it to `MetaMod' as `NewName'.
-spec curry_add(MetaMod, Module, Function, Arity, Args, NewName) -> Result when
MetaMod :: meta_mod(),
Module :: module() | meta_mod(),
Function :: atom(),
Arity :: arity(),
Args :: args(),
NewName :: atom(),
Result :: result(meta_mod()).
curry_add(MetaMod, Module, Name, Arity, Args, NewName) ->
case curry(Module, Name, Arity, Args, NewName) of
{ok, Form} ->
add_func(MetaMod, Form);
Err ->
Err
end.
%% @doc Replace the function represented by `Form' in `MetaMod'
%% with its curried form.
-spec curry_replace(MetaMod, Form, Args) -> result(meta_mod()) when
MetaMod :: meta_mod(),
Form :: func_form(),
Args :: args().
curry_replace(MetaMod, {function, _Line, Name, Arity, _Clauses}, Args) ->
curry_replace(MetaMod, Name, Arity, Args).
%% @doc Replace `Function'/`Arity' in `MetaMod' with its curried form.
-spec curry_replace(MetaMod, Function, Arity, Args) -> result(meta_mod()) when
MetaMod :: meta_mod(),
Function :: atom(),
Arity :: arity(),
Args :: args().
curry_replace(MetaMod, Name, Arity, Args) ->
curry_change(MetaMod, Name, Arity, Args, true).
%% @doc Replace the arguments of the function represented by `Form',
%% where the argument's `Name' matches an element from `Vals'
%% with the corresponding `Value'.
-spec embed_args(Form, Vals) -> NewForm when
Form :: func_form(),
Vals :: [{Name :: atom(), Value :: term()}],
NewForm :: func_form().
embed_args({function, L, Name, Arity, Clauses}, Vals) ->
NewClauses = new_clauses(Clauses, Vals),
NewArity = case NewClauses of
[{clause, _L2, Args, _Guards, _Exprs}|_] ->
length(Args);
_ ->
Arity
end,
{function, L, Name, NewArity, NewClauses}.
%% @equiv embed_args(MetaMod, Name, Arity, Values, Name)
-spec embed_args(MetaMod, Function, Arity, Values) -> result(meta_mod()) when
MetaMod :: meta_mod(),
Function :: atom(),
Arity :: arity(),
Values :: proplists:proplist().
embed_args(MetaMod, Name, Arity, Values) ->
embed_args(MetaMod, Name, Arity, Values, Name).
%% @doc Apply {@link embed_args/2} to `MetaMod':`Function'/`Arity' and
%% add the resulting function to `MetMod', after renaming it to `NewName'.
%% @see rename/2
-spec embed_args(MetaMod, Function, Arity, Values, NewName) -> Result when
MetaMod :: meta_mod(),
Function :: atom(),
Arity :: arity(),
Values :: proplists:proplist(),
NewName :: atom(),
Result :: result(meta_mod()).
embed_args(MetaMod, Name, Arity, Values, NewName) ->
case get_func(MetaMod, Name, Arity) of
{ok, Form} ->
NewForm = embed_args(Form, Values),
add_func(MetaMod, rename(NewForm, NewName));
Err ->
Err
end.
%% @doc Apply {@link embed_args/2} with `Values' to all forms in `MetaMod'.
%% `exports' for functions whose arities change are preserved.
-spec embed_all(MetaMod, Values) -> NewMod when
MetaMod :: meta_mod(),
Values :: [{Name :: atom(), Value :: term()}],
NewMod :: meta_mod().
embed_all(MetaMod, Vals) ->
Forms = get_forms(MetaMod),
Exports = get_exports(MetaMod),
{NewForms, Exports3, NewExports} =
lists:foldl(
fun({function, _L, Name, Arity, _Clauses} = Form,
{Forms1, Exports1, NewExports1}) ->
{function, _, _, NewArity, _} = NewForm =
embed_args(Form, Vals),
Exports2 = lists:delete({Name, Arity}, Exports1),
NewExports2 =
?IF(length(Exports2) == length(Exports1),
NewExports1,
[{Name, NewArity} | NewExports1]),
{[NewForm | Forms1], Exports2, NewExports2};
(Form, {Forms1, Exports1, NewExports1}) ->
{[Form | Forms1], Exports1, NewExports1}
end, {[], Exports, []}, Forms),
#meta_mod{module = get_module(MetaMod),
exports = Exports3 ++ NewExports,
forms = lists:reverse(NewForms),
export_all = get_export_all(MetaMod)}.
%% @doc Add aliases for `Parent''s functions missing from `Child' to `Child'.
%% The new functions in `Child' are shallow, i.e. they have the name and arity
%% of the corresponding functions in `Parent', but instead of implementing their
%% logic they call the `Parent' functions.
-spec extend(Parent, Child) -> NewChildMod when
Parent :: module() | meta_mod(),
Child :: module() | meta_mod(),
NewChildMod :: meta_mod().
extend(Parent, Child) ->
extend(Parent, Child, 0).
%% @doc Similar to {@link extend/2}, with the addition of `ArityDiff', which
%% indicates the difference in arities <em>Smerl</em> should use when figuring
%% out which functions to generate based on the modules' exports. This is
%% sometimes useful when calling {@link extend/3} followed by {@link
%% embed_all/2}.
-spec extend(Parent, Child, ArityDiff) -> NewChildMod when
Parent :: module() | meta_mod(),
Child :: module() | meta_mod(),
ArityDiff :: non_neg_integer(),
NewChildMod :: meta_mod().
extend(Parent, Child, ArityDiff) ->
extend(Parent, Child, ArityDiff, []).
-spec extend(Parent, Child, ArityDiff, Options) -> NewChildMod when
Parent :: module() | meta_mod(),
Child :: module() | meta_mod(),
ArityDiff :: non_neg_integer(),
Options :: [proplists:property()],
NewChildMod :: meta_mod().
extend(Parent, Child, ArityDiff, Options) ->
{{ParentName, ParentExports, ParentMod}, ChildMod} =
get_extend_data(Parent, Child, Options),
ChildExports = get_exports(ChildMod),
ChildExports1 = [{ExportName, ExportArity + ArityDiff} ||
{ExportName, ExportArity} <-
ChildExports],
ExportsDiff = ParentExports -- ChildExports1,
NewChild =
lists:foldl(
fun({FuncName, Arity}, ChildMod1) ->
Func =
case lists:member(copy, Options) of
true ->
{ok, ParentFunc} =
smerl:get_func(ParentMod, FuncName, Arity),
ParentFunc;
_ ->
Args = get_args(
ParentMod, FuncName, Arity),
Clause1 =
{clause, 1, Args, [],
[{call, 1,
{remote, 1, {atom, 1, ParentName},
{atom, 1, FuncName}},
Args}]},
{function, 1, FuncName, Arity, [Clause1]}
end,
{ok, ChildMod2} = add_func(ChildMod1, Func),
ChildMod2
end, ChildMod, ExportsDiff),
NewChild.
%% @doc Return the pretty-printed source code for `MetaMod'.
-spec to_src(MetaMod :: meta_mod()) -> Source :: string().
to_src(MetaMod) ->
ExportsForm = {attribute, 1, export, get_exports(MetaMod)},
AllForms = [{attribute, 1, module, get_module(MetaMod)}, ExportsForm |
get_forms(MetaMod)],
erl_prettypr:format(erl_syntax:form_list(AllForms)).
%% @equiv file:write_file(Filename , to_src(MetaMod))
-spec to_src(MetaMod, Filename) -> error_t(term()) when
MetaMod :: meta_mod(),
Filename :: file:filename().
to_src(MetaMod, Filename) ->
file:write_file(Filename, to_src(MetaMod)).
%%% ===================================================== [ Internal functions ]
get_module_forms(ModuleName) ->
case code:which(ModuleName) of
Path when is_list(Path) ->
case get_forms(ModuleName, Path) of
{ok, Forms} -> mod_for_forms(Forms);
_Error -> {error, {invalid_module, ModuleName}}
end;
_Error ->
{error, {invalid_module, ModuleName}}
end.
mod_for_forms([{attribute, _, file, {FileName, _FileNum}},
{attribute, _, module, ModuleName}|Forms]) ->
{Exports, OtherForms, ExportAll} =
lists:foldl(
fun({attribute, _, export, ExportList},
{ExportsAcc, FormsAcc, ExportAll}) ->
{ExportList ++ ExportsAcc, FormsAcc, ExportAll};
({attribute, _, compile, export_all},
{ExportsAcc, FormsAcc, _ExportAll}) ->
{ExportsAcc, FormsAcc, true};
({eof, _}, Acc) ->
Acc;
(Form, {ExportsAcc, FormsAcc, ExportAll}) ->
{ExportsAcc, [Form | FormsAcc], ExportAll}
end, {[], [], false}, Forms),
{ok, #meta_mod{module = ModuleName,
file = FileName,
exports = Exports,
forms = OtherForms,
export_all = ExportAll
}};
mod_for_forms(Mod) ->
{error, {invalid_module, Mod}}.
%% @doc Get the abstract representation, if available, of `Module'.
%%
%% Strategy:
%% <ol>
%% <li>Try to get the abstract code from `Module' if it's compiled with
%% `debug_info'.</li>
%% <li>Look for the source file in the beam file's directory.</li>
%% <li>If the file's directory ends with `ebin', then search in
%% `[beamdir]/../src'</li>
%% </ol>
get_forms(Module, Path) ->
case beam_lib:chunks(Path, [abstract_code]) of
{ok, {_, [{abstract_code, {raw_abstract_v1, Forms}}]}} ->
{ok, Forms};
_Err ->
case filelib:find_source(Module) of
{error, _} = Err ->
get_forms_from_binary(Module, Err);
{ok, Filename} ->
epp:parse_file(Filename, [filename:dirname(Filename)], [])
end
end.
%% @doc Try to infer module source files from the beam code path.
get_forms_from_binary(Module, OrigErr) ->
Ret =
case code:where_is_file(atom_to_list(Module) ++ ".beam") of
non_existing ->
OrigErr;
Filename ->
%% We could automatically obtain a list of all dirs under this dir,
%%but we just do a hack for now.
Basedir = filename:dirname(Filename),
Lastdir = filename:basename(Basedir),
case Lastdir of
"ebin" ->
Rootdir = filename:dirname(Basedir),
DirList0 = [Rootdir ++ "/src"],
DirList = DirList0 ++ get_dirs_in_dir(Rootdir ++ "/src"),
get_forms_from_file_list(Module, Rootdir, DirList);
_ ->
DirList = [Basedir],
get_forms_from_file_list(Module, Basedir, DirList)
end
end,
case Ret of
[] -> OrigErr;
_ -> Ret
end.
get_dirs_in_dir(Dir) ->
case list_dir(Dir) of
{ok, Listing} ->
lists:filtermap(fun(Name) -> is_directory(Dir, Name) end, Listing);
{error, _} ->
undefined
end.
%% @equiv is_directory(filename:join(Directory, Filename))
is_directory(Directory, Filename) ->
is_directory(filename:join(Directory, Filename)).
is_directory(Filename) ->
case read_file_info(Filename) of
{ok, #file_info{type=directory}} -> true;
_ -> false
end.
get_forms_from_file_list(_Module, _Basedir, []) ->
[];
get_forms_from_file_list(Module, Basedir, [H|T]) ->
Filename = H ++ "/" ++ atom_to_list(Module) ++ ".erl",
case read_file_info(Filename) of
{ok, #file_info{type=regular}} ->
epp:parse_file(Filename, [filename:dirname(Filename)], []);
_ ->
get_forms_from_file_list(Module, Basedir, T)
end.
do_get_func([], FuncName, Arity) ->
{error, {function_not_found, {FuncName, Arity}}};
do_get_func([{function, _Line, FuncName, Arity, _Clauses} = Form | _Rest],
FuncName, Arity) ->
{ok, Form};
do_get_func([_Form|Rest], FuncName, Arity) ->
do_get_func(Rest, FuncName, Arity).
parse_func_string(Func) ->
case erl_scan:string(Func) of
{ok, Toks, _} ->
case erl_parse:parse_form(Toks) of
{ok, _Form} = Res ->
Res;
_Err ->
{error, parse_error}
end;
_Err ->
{error, parse_error}
end.
compile(ModName, Forms, Options) ->
case compile:forms(Forms, Options) of
{ok, Module, Bin} ->
OutDir = proplists:get_value(outdir, Options),
maybe_write_beam_file({ModName, Module}, Bin, OutDir);
Err ->
Err
end.
maybe_write_beam_file({_ModName, Module}, Bin, undefined) ->
Filename = case code:which(Module) of
non_existing -> atom_to_list(Module) ++ ".erl";
Path -> Path
end,
code:purge(Module),
case code:load_binary(Module, Filename, Bin) of
{module, _Module} -> ok;
Err -> Err
end;
maybe_write_beam_file({ModName, _Module}, Bin, OutDir) ->
BeamFile = atom_to_list(ModName) ++ ".beam",
file:write_file(filename:join(OutDir, BeamFile), Bin).
curry_clause({clause, L1, ExistingArgs, Guards, _Exprs} = Clause, NewArgs) ->
{FirstArgs, LastArgs} = lists:split(length(NewArgs), ExistingArgs),
Vals = [ {Name, erl_parse:abstract(NewVal)}
|| {{var, _ , Name}, NewVal} <- lists:zip(FirstArgs, NewArgs) ],
NewExprs = replace_vars(Clause, Vals),
{clause, L1, LastArgs, Guards, NewExprs}.
replace_vars(Clause, Vals) ->
Tree =
erl_syntax_lib:map(
fun({var, _L2, Name} = Expr) ->
case proplists:lookup(Name, Vals) of
none ->
Expr;
{_, Val} ->
Val
end;
(Expr) ->
Expr
end, Clause),
{clause, _, _, _, NewExprs} = erl_syntax:revert(Tree),
NewExprs.
curry_change(MetaMod, Name, Arity, Args, Remove) ->
case get_func(MetaMod, Name, Arity) of
{ok, OldForm} ->
case curry(OldForm, Args) of
{ok, NewForm} ->
MetaMod1 =
?IF(Remove, remove_func(MetaMod, Name, Arity), MetaMod),
add_func(MetaMod1, NewForm);
Err ->
Err
end;
Err ->
Err
end.
%% @see embed_args/2
new_clauses(Clauses, Vals) ->
[ begin
{EmbeddedVals, OtherArgs} =
lists:foldr(
fun({var, _, VarName} = Arg, {Embedded, Rest}) ->
case proplists:lookup(VarName, Vals) of
none ->
{Embedded, [Arg | Rest]};
{_, Val} ->
{[{VarName, erl_parse:abstract(Val)} |
Embedded], Rest}
end;
(Arg, {Embedded, Rest}) ->
{Embedded, [Arg | Rest]}
end, {[], []}, Args),
NewExprs = replace_vars(Clause, EmbeddedVals),
{clause, L1, OtherArgs, Guards, NewExprs}
end || {clause, L1, Args, Guards, _Exprs} = Clause <- Clauses].
get_extend_data(Parent, Child, Options) when is_atom(Parent) ->
SrcDir = proplists:get_value(src_dir, Options),
do_get_extend_data(Parent, Child, Options, SrcDir);
get_extend_data(Parent, Child, Options) when is_record(Parent, meta_mod) ->
Data = {get_module(Parent), get_exports(Parent), Parent},
get_extend_data(Data, Child, Options);
get_extend_data(Parent, Child, Options) when is_list(Parent) ->
case for_file(Parent) of
{ok, M1} ->
get_extend_data(M1, Child, Options);
Err ->
Err
end;
get_extend_data({_, _, _} = ParentData, Child, _Options)
when is_atom(Child); is_list(Child) ->
case for_module(Child) of
{ok, MetaMod} ->
{ParentData, MetaMod};
Err ->
Err
end;
get_extend_data(ParentData, Child, _Options) when is_record(Child, meta_mod) ->
{ParentData, Child}.
do_get_extend_data(Parent, Child, Options, undefined) ->
[{exports, Exports} |_] = Parent:module_info(),
Exports1 = Exports -- [{module_info, 0}],
Exports2 = Exports1 -- [{module_info, 1}],
ParentMod = case smerl:for_module(Parent) of
{ok, M} -> M;
{error, _} -> undefined
end,
get_extend_data({Parent, Exports2, ParentMod}, Child, Options);
do_get_extend_data(Parent, Child, Options, Dir) ->
Filename = filename:join(Dir, atom_to_list(Parent) ++ ".erl"),
%% Check if file exists
case read_file_info(Filename) of
{ok, _FileInfo} ->
ParentMod = case smerl:for_file(Filename) of
{ok, MetaMod} -> MetaMod;
{error, _} -> undefined
end,
get_extend_data(ParentMod, Child, Options);
Error ->
Error
end.
get_args(_, _, 0) -> [];
get_args(undefined, _FuncName, Arity) ->
[{var, 1, list_to_atom("P" ++ integer_to_list(Num))}
|| Num <- lists:seq(1, Arity)];
get_args(ParentMod, FuncName, Arity) ->
{ok, {function, _L, _Name, _Arity,
[{clause, _, Args, _Guards, _Exprs} | _]}} =
get_func(ParentMod, FuncName, Arity),
Args.
-spec list_dir(Dir :: file:dirname()) -> Result when
Result :: result([file:filename()], erl_prim_loader).
list_dir(Dir) ->
handle_list_dir(erl_prim_loader:list_dir(Dir)).
handle_list_dir(error) -> {error, erl_prim_loader};
handle_list_dir({ok, Filenames}) -> {ok, Filenames}.
-spec read_file_info(Filename :: file:filename()) -> Result when
Result :: result(file:file_info(), file:posix() | badarg).
read_file_info(Filename) ->
erl_prim_loader:read_file_info(Filename).
%%% ==================================================================== [ EOF ]