Current section

Files

Jump to
util src smerl.erl
Raw

src/smerl.erl

%%% ============================================================== [ smerl.erl ]
-module(smerl).
-moduledoc """
Simple Metaprogramming for Erlang
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>
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 ]
Author: Yariv Sadan
Copyright: 2006-2007, 2016 AUTHORS
""".
-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()}.
-doc "A list of `export()`s.".
-type exports() :: [export()].
-doc "The abstract form for the function, as described in the ERTS Users' manual.".
-type func_form() :: erl_parse:abstract_form().
-doc "A list of `func_form()`s.".
-type func_forms() :: [func_form()].
%% 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()
}).
-doc "A data structure holding the abstract representation for a module.".
-type meta_mod() :: #meta_mod{}.
-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 `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
`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 `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:
- Try to get the abstract code from `Module` if it's compiled with
- Look for the source file in the beam file's directory.
- If the file's directory ends with `ebin`, then search in
`[beamdir]/../src`
""".
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 ]