Erlang's typespecs define only cover_compiled and preloaded as valid atoms and uses an empty list for generated code. So we should follow Erlang's rules until we can officially relax them.
389 lines
13 KiB
Erlang
389 lines
13 KiB
Erlang
-module(elixir_module).
|
|
-export([data_table/1, defs_table/1, is_open/1, delete_doc/6,
|
|
compile/4, expand_callback/6, format_error/1,
|
|
compiler_modules/0, delete_impl/6,
|
|
write_cache/3, read_cache/2]).
|
|
-include("elixir.hrl").
|
|
|
|
-define(lexical_attr, {elixir, lexical_tracker}).
|
|
-define(persisted_attr, {elixir, persisted_attributes}).
|
|
|
|
%% Stores modules currently being defined by the compiler
|
|
|
|
compiler_modules() ->
|
|
case erlang:get(elixir_compiler_modules) of
|
|
undefined -> [];
|
|
M when is_list(M) -> M
|
|
end.
|
|
|
|
put_compiler_modules([]) ->
|
|
erlang:erase(elixir_compiler_modules);
|
|
put_compiler_modules(M) when is_list(M) ->
|
|
erlang:put(elixir_compiler_modules, M).
|
|
|
|
%% Table functions
|
|
|
|
data_table(Module) ->
|
|
ets:lookup_element(elixir_modules, Module, 2).
|
|
|
|
defs_table(Module) ->
|
|
ets:lookup_element(elixir_modules, Module, 3).
|
|
|
|
is_open(Module) ->
|
|
ets:lookup(elixir_modules, Module) /= [].
|
|
|
|
delete_doc(#{module := Module}, _, _, _, _, _) ->
|
|
ets:delete(data_table(Module), doc),
|
|
ok.
|
|
|
|
delete_impl(#{module := Module}, _, _, _, _, _) ->
|
|
ets:delete(data_table(Module), impl),
|
|
ok.
|
|
|
|
write_cache(Module, Key, Value) ->
|
|
ets:insert(data_table(Module), {{cache, Key}, Value}).
|
|
|
|
read_cache(Module, Key) ->
|
|
ets:lookup_element(data_table(Module), {cache, Key}, 2).
|
|
|
|
%% Compilation hook
|
|
|
|
compile(Module, Block, Vars, #{line := Line} = Env) when is_atom(Module) ->
|
|
%% In case we are generating a module from inside a function,
|
|
%% we get rid of the lexical tracker information as, at this
|
|
%% point, the lexical tracker process is long gone.
|
|
LexEnv = case ?key(Env, function) of
|
|
nil -> Env#{module := Module};
|
|
_ -> Env#{lexical_tracker := nil, function := nil, module := Module}
|
|
end,
|
|
|
|
case ?key(LexEnv, lexical_tracker) of
|
|
nil ->
|
|
elixir_lexical:run(?key(LexEnv, file), nil, fun(Pid) ->
|
|
compile(Line, Module, Block, Vars, LexEnv#{lexical_tracker := Pid})
|
|
end);
|
|
_ ->
|
|
compile(Line, Module, Block, Vars, LexEnv)
|
|
end;
|
|
compile(Module, _Block, _Vars, #{line := Line, file := File}) ->
|
|
elixir_errors:form_error([{line, Line}], File, ?MODULE, {invalid_module, Module}).
|
|
|
|
compile(Line, Module, Block, Vars, E) ->
|
|
File = ?key(E, file),
|
|
check_module_availability(Line, File, Module),
|
|
|
|
CompilerModules = compiler_modules(),
|
|
{Data, Defs, Ref} = build(Line, File, Module, ?key(E, lexical_tracker)),
|
|
|
|
try
|
|
put_compiler_modules([Module | CompilerModules]),
|
|
{Result, NE, OverridablePairs} = eval_form(Line, Module, Data, Block, Vars, E),
|
|
|
|
PersistedAttributes = ets:lookup_element(Data, ?persisted_attr, 2),
|
|
Attributes = attributes(Line, File, Data, PersistedAttributes),
|
|
OnLoad = ets:lookup_element(Data, 'on_load', 2),
|
|
[elixir_locals:record_local(Tuple, Module) || Tuple <- OnLoad],
|
|
|
|
{AllDefinitions, Unreachable} = elixir_def:fetch_definitions(File, Module),
|
|
|
|
(not elixir_config:get(bootstrap)) andalso
|
|
'Elixir.Module':check_behaviours_and_impls(E, Data, AllDefinitions, OverridablePairs),
|
|
|
|
CompileOpts = lists:flatten(ets:lookup_element(Data, compile, 2)),
|
|
|
|
ModuleMap = #{
|
|
module => Module,
|
|
line => Line,
|
|
file => File,
|
|
attributes => Attributes,
|
|
definitions => AllDefinitions,
|
|
unreachable => Unreachable,
|
|
compile_opts => CompileOpts
|
|
},
|
|
|
|
Binary = elixir_erl:compile(ModuleMap),
|
|
warn_unused_attributes(File, Data, PersistedAttributes),
|
|
autoload_module(Module, Binary, CompileOpts, NE),
|
|
eval_callbacks(Line, Data, after_compile, [NE, Binary], NE),
|
|
make_module_available(Module, Binary),
|
|
{module, Module, Binary, Result}
|
|
catch
|
|
error:undef ->
|
|
case erlang:get_stacktrace() of
|
|
[{Module, Fun, Args, _Info} | _] = Stack when is_list(Args) ->
|
|
compile_undef(Module, Fun, length(Args), Stack);
|
|
[{Module, Fun, Arity, _Info} | _] = Stack ->
|
|
compile_undef(Module, Fun, Arity, Stack);
|
|
Stack ->
|
|
erlang:raise(error, undef, Stack)
|
|
end
|
|
after
|
|
put_compiler_modules(CompilerModules),
|
|
elixir_locals:cleanup(Module),
|
|
ets:delete(Data),
|
|
ets:delete(Defs),
|
|
elixir_code_server:call({undefmodule, Ref})
|
|
end.
|
|
|
|
%% An undef error for a function in the module being compiled might result in an
|
|
%% exception message suggesting the current module is not loaded. This is
|
|
%% misleading so use a custom reason.
|
|
compile_undef(Module, Fun, Arity, Stack) ->
|
|
ExMod = 'Elixir.UndefinedFunctionError',
|
|
case code:ensure_loaded(ExMod) of
|
|
{module, _} ->
|
|
Opts = [{module, Module}, {function, Fun}, {arity, Arity},
|
|
{reason, 'function not available'}],
|
|
Exception = 'Elixir.UndefinedFunctionError':exception(Opts),
|
|
erlang:raise(error, Exception, Stack);
|
|
{_, _} ->
|
|
erlang:raise(error, undef, Stack)
|
|
end.
|
|
|
|
%% Handle reserved modules and duplicates.
|
|
|
|
check_module_availability(Line, File, Module) ->
|
|
Reserved = ['Elixir.Any', 'Elixir.BitString', 'Elixir.Function', 'Elixir.PID',
|
|
'Elixir.Reference', 'Elixir.Elixir', 'Elixir'],
|
|
|
|
case lists:member(Module, Reserved) of
|
|
true -> elixir_errors:form_error([{line, Line}], File, ?MODULE, {module_reserved, Module});
|
|
false -> ok
|
|
end,
|
|
|
|
case elixir_compiler:get_opt(ignore_module_conflict) of
|
|
false ->
|
|
case code:ensure_loaded(Module) of
|
|
{module, _} ->
|
|
elixir_errors:form_warn([{line, Line}], File, ?MODULE, {module_defined, Module});
|
|
{error, _} ->
|
|
ok
|
|
end;
|
|
true ->
|
|
ok
|
|
end.
|
|
|
|
%% Hook that builds both attribute and functions and set up common hooks.
|
|
|
|
build(Line, File, Module, Lexical) ->
|
|
case elixir_code_server:call({lookup, Module}) of
|
|
[{Module, _, _, OldLine, OldFile}] ->
|
|
Error = {module_in_definition, Module, OldFile, OldLine},
|
|
elixir_errors:form_error([{line, Line}], File, ?MODULE, Error);
|
|
_ ->
|
|
[]
|
|
end,
|
|
|
|
Data = ets:new(Module, [set, public]),
|
|
Defs = ets:new(Module, [duplicate_bag, public]),
|
|
Ref = elixir_code_server:call({defmodule, self(),
|
|
{Module, Data, Defs, Line, File}}),
|
|
|
|
DocsOnDefinition =
|
|
case elixir_compiler:get_opt(docs) of
|
|
true -> [{'Elixir.Module', compile_doc}];
|
|
_ -> [{elixir_module, delete_doc}]
|
|
end,
|
|
|
|
ImplOnDefinition =
|
|
case elixir_config:get(bootstrap) of
|
|
true -> [{elixir_module, delete_impl}];
|
|
_ -> [{'Elixir.Module', compile_impl}]
|
|
end,
|
|
|
|
%% Docs must come first as they read the impl callback.
|
|
OnDefinition = DocsOnDefinition ++ ImplOnDefinition,
|
|
|
|
ets:insert(Data, [
|
|
% {Key, Value, Accumulate?, UnreadLine}
|
|
{after_compile, [], true, nil},
|
|
{before_compile, [], true, nil},
|
|
{behaviour, [], true, nil},
|
|
{compile, [], true, nil},
|
|
{derive, [], true, nil},
|
|
{dialyzer, [], true, nil},
|
|
{external_resource, [], true, nil},
|
|
{moduledoc, nil, false, nil},
|
|
{on_definition, OnDefinition, true, nil},
|
|
{on_load, [], true, nil},
|
|
|
|
% Types
|
|
{callback, [], true, nil},
|
|
{opaque, [], true, nil},
|
|
{optional_callbacks, [], true, nil},
|
|
{macrocallback, [], true, nil},
|
|
{spec, [], true, nil},
|
|
{type, [], true, nil},
|
|
{typep, [], true, nil},
|
|
|
|
% Internal
|
|
{{elixir, impls}, []},
|
|
{{elixir, cache_env}, 0, nil}
|
|
]),
|
|
|
|
Persisted = [behaviour, on_load, compile, external_resource, dialyzer, vsn],
|
|
ets:insert(Data, {?persisted_attr, Persisted}),
|
|
ets:insert(Data, {?lexical_attr, Lexical}),
|
|
|
|
%% Setup definition related modules
|
|
elixir_def:setup(Module),
|
|
elixir_locals:setup(Module),
|
|
elixir_overridable:setup(Module),
|
|
|
|
{Data, Defs, Ref}.
|
|
|
|
%% Handles module and callback evaluations.
|
|
|
|
eval_form(Line, Module, Data, Block, Vars, E) ->
|
|
{Value, EE} = elixir_compiler:eval_forms(Block, Vars, E),
|
|
Pairs1 = elixir_overridable:store_pending(Module),
|
|
EV = elixir_env:linify({Line, reset_env(EE)}),
|
|
EC = eval_callbacks(Line, Data, before_compile, [EV], EV),
|
|
Pairs2 = elixir_overridable:store_pending(Module),
|
|
OverridablePairs = Pairs1 ++ Pairs2,
|
|
{Value, EC, OverridablePairs}.
|
|
|
|
eval_callbacks(Line, Data, Name, Args, E) ->
|
|
Callbacks = ets:lookup_element(Data, Name, 2),
|
|
lists:foldr(fun({M, F}, Acc) ->
|
|
expand_callback(Line, M, F, Args, reset_env(Acc),
|
|
fun(AM, AF, AA) -> apply(AM, AF, AA) end)
|
|
end, E, Callbacks).
|
|
|
|
expand_callback(Line, M, F, Args, E, Fun) ->
|
|
Meta = [{line, Line}, {required, true}],
|
|
|
|
{EE, ET} = elixir_dispatch:dispatch_require(Meta, M, F, Args, E, fun(AM, AF, AA) ->
|
|
Fun(AM, AF, AA),
|
|
{ok, E}
|
|
end),
|
|
|
|
if
|
|
is_atom(EE) ->
|
|
ET;
|
|
true ->
|
|
try
|
|
{_Value, _Binding, EF, _S} = elixir:eval_forms(EE, [], ET),
|
|
EF
|
|
catch
|
|
Kind:Reason ->
|
|
Stacktrace = erlang:get_stacktrace(),
|
|
Info = {M, F, length(Args), location(Line, E)},
|
|
erlang:raise(Kind, Reason, prune_stacktrace(Info, Stacktrace))
|
|
end
|
|
end.
|
|
|
|
reset_env(Env) ->
|
|
Env#{vars := [], export_vars := nil}.
|
|
|
|
%% Add attributes handling to the form
|
|
|
|
attributes(Line, File, Data, PersistedAttributes) ->
|
|
[{Key, Value} || Key <- PersistedAttributes,
|
|
Value <- lookup_attribute(Line, File, Data, Key)].
|
|
|
|
lookup_attribute(Line, File, Data, Key) when is_atom(Key) ->
|
|
case ets:lookup(Data, Key) of
|
|
[{resource, Values, true, _}] ->
|
|
lists:usort([validate_external_resource(Line, File, Value) || Value <- Values]);
|
|
[{Key, Values, true, _}] ->
|
|
Values;
|
|
[{Key, Value, false, _}] ->
|
|
[Value];
|
|
[] ->
|
|
[]
|
|
end.
|
|
|
|
validate_external_resource(_Line, _File, Value) when is_binary(Value) ->
|
|
Value;
|
|
validate_external_resource(Line, File, Value) ->
|
|
elixir_errors:form_error([{line, Line}], File, ?MODULE, {invalid_external_resource, Value}).
|
|
|
|
%% Takes care of autoloading the module if configured.
|
|
|
|
autoload_module(Module, Binary, Opts, E) ->
|
|
case proplists:get_value(autoload, Opts, true) of
|
|
true -> code:load_binary(Module, beam_location(E), Binary);
|
|
false -> ok
|
|
end.
|
|
|
|
beam_location(#{lexical_tracker := Pid, module := Module}) ->
|
|
case elixir_lexical:dest(Pid) of
|
|
nil -> "";
|
|
Dest ->
|
|
filename:join(elixir_utils:characters_to_list(Dest),
|
|
atom_to_list(Module) ++ ".beam")
|
|
end.
|
|
|
|
%% Handle unused attributes warnings and special cases.
|
|
|
|
warn_unused_attributes(File, Data, PersistedAttrs) ->
|
|
ReservedAttrs = [after_compile, before_compile, moduledoc, on_definition | PersistedAttrs],
|
|
Keys = ets:select(Data, [{{'$1', '_', '_', '$2'}, [{is_atom, '$1'}, {is_integer, '$2'}], [['$1', '$2']]}]),
|
|
[elixir_errors:form_warn([{line, Line}], File, ?MODULE, {unused_attribute, Key}) ||
|
|
[Key, Line] <- Keys, not lists:member(Key, ReservedAttrs)].
|
|
|
|
%% Integration with elixir_compiler that makes the module available
|
|
|
|
make_module_available(Module, Binary) ->
|
|
case get(elixir_module_binaries) of
|
|
Current when is_list(Current) ->
|
|
put(elixir_module_binaries, [{Module, Binary} | Current]),
|
|
|
|
case get(elixir_compiler_pid) of
|
|
undefined ->
|
|
ok;
|
|
PID ->
|
|
Ref = make_ref(),
|
|
PID ! {module_available, self(), Ref, get(elixir_compiler_file), Module, Binary},
|
|
receive {Ref, ack} -> ok end
|
|
end;
|
|
_ ->
|
|
ok
|
|
end.
|
|
|
|
%% Error handling and helpers.
|
|
|
|
%% We've reached the elixir_module or eval internals, skip it with the rest
|
|
prune_stacktrace(Info, [{elixir, eval_forms, _, _} | _]) ->
|
|
[Info];
|
|
prune_stacktrace(Info, [{elixir_module, _, _, _} | _]) ->
|
|
[Info];
|
|
prune_stacktrace(Info, [H | T]) ->
|
|
[H | prune_stacktrace(Info, T)];
|
|
prune_stacktrace(Info, []) ->
|
|
[Info].
|
|
|
|
location(Line, E) ->
|
|
[{file, elixir_utils:characters_to_list(?key(E, file))}, {line, Line}].
|
|
|
|
format_error({invalid_external_resource, Value}) ->
|
|
io_lib:format("expected a string value for @external_resource, got: ~p",
|
|
['Elixir.Kernel':inspect(Value)]);
|
|
format_error({unused_attribute, typedoc}) ->
|
|
"module attribute @typedoc was set but no type follows it";
|
|
format_error({unused_attribute, doc}) ->
|
|
"module attribute @doc was set but no definition follows it";
|
|
format_error({unused_attribute, impl}) ->
|
|
"module attribute @impl was set but no definition follows it";
|
|
format_error({unused_attribute, Attr}) ->
|
|
io_lib:format("module attribute @~ts was set but never used", [Attr]);
|
|
format_error({invalid_module, Module}) ->
|
|
io_lib:format("invalid module name: ~ts", ['Elixir.Kernel':inspect(Module)]);
|
|
format_error({module_defined, Module}) ->
|
|
Extra =
|
|
case code:which(Module) of
|
|
"" ->
|
|
" (current version defined in memory)";
|
|
Path when is_list(Path) ->
|
|
io_lib:format(" (current version loaded from ~ts)", [elixir_utils:relative_to_cwd(Path)]);
|
|
_ ->
|
|
""
|
|
end,
|
|
io_lib:format("redefining module ~ts~ts", [elixir_aliases:inspect(Module), Extra]);
|
|
format_error({module_reserved, Module}) ->
|
|
io_lib:format("module ~ts is reserved and cannot be defined", [elixir_aliases:inspect(Module)]);
|
|
format_error({module_in_definition, Module, File, Line}) ->
|
|
io_lib:format("cannot define module ~ts because it is currently being defined in ~ts:~B",
|
|
[elixir_aliases:inspect(Module), elixir_utils:relative_to_cwd(File), Line]).
|