Previously, since the functions were not concrete but stubs replaced at compilation time, we had to special the module definition to not actually include the specs, which would become compilation errors. Now we provide a concrete implementation of the functions and therefore the spec will be available in the final source. Those implementations are not actually invoked in practice, since they are still transformed into erlang ones for performance reasons.
360 lines
12 KiB
Erlang
360 lines
12 KiB
Erlang
-module(elixir_module).
|
|
-export([translate/4, compile/5, data_table/1, eval_quoted/4, format_error/1]).
|
|
-include("elixir.hrl").
|
|
-compile({parse_transform, elixir_transform}).
|
|
|
|
eval_quoted(Module, Quoted, RawBinding, Opts) ->
|
|
Binding = binding_for_eval(Module, RawBinding),
|
|
Scope = scope_for_eval(Module, Opts),
|
|
|
|
elixir_def:reset_last(Module),
|
|
|
|
case orddict:find(line, Opts) of
|
|
{ ok, Line } -> Line;
|
|
error -> Line = 1
|
|
end,
|
|
|
|
{ Value, FinalBinding, _Scope } = elixir:eval_quoted([Quoted], Binding, Line, Scope),
|
|
{ Value, FinalBinding }.
|
|
|
|
scope_for_eval(Module, #elixir_scope{} = S) ->
|
|
S#elixir_scope{module=Module};
|
|
|
|
scope_for_eval(Module, Opts) ->
|
|
scope_for_eval(Module, elixir:scope_for_eval(Opts)).
|
|
|
|
binding_for_eval(Module, Binding) -> [{'_@MODULE',Module}|Binding].
|
|
|
|
|
|
%% TABLE METHODS
|
|
|
|
data_table(Module) ->
|
|
Module.
|
|
|
|
docs_table(Module) ->
|
|
?ELIXIR_ATOM_CONCAT([o, Module]).
|
|
|
|
spec_table(Module) ->
|
|
?ELIXIR_ATOM_CONCAT([s, Module]).
|
|
|
|
%% TRANSFORMATION FUNCTIONS
|
|
|
|
%% Transformation of args and scope into a compiled erlang call.
|
|
%% The abstract form for extra arguments may be given and they
|
|
%% will be passed to the invoked function.
|
|
|
|
translate(Line, Ref, Block, S) ->
|
|
MetaBlock = elixir_tree_helpers:abstract_syntax(Block),
|
|
MetaS = elixir_scope:serialize(S),
|
|
|
|
Vars = orddict:fold(fun(Key, Value, { Acc, Counter }) ->
|
|
{ { cons, Line, { tuple, Line, [
|
|
{ atom, Line, Key },
|
|
{ atom, Line, ?ELIXIR_ATOM_CONCAT(["_@", Counter]) },
|
|
{ var, Line, Value }
|
|
] }, Acc }, Counter + 1 }
|
|
end, { { nil, Line }, 0 }, S#elixir_scope.vars),
|
|
|
|
Args = [{integer, Line, Line}, Ref, MetaBlock, element(1, Vars), MetaS],
|
|
?ELIXIR_WRAP_CALL(Line, ?MODULE, compile, Args).
|
|
|
|
%% The compilation hook.
|
|
|
|
compile(Line, Module, Block, Vars, #elixir_scope{} = S) when is_atom(Module) ->
|
|
C = elixir_compiler:get_opts(),
|
|
File = S#elixir_scope.file,
|
|
|
|
check_module_availability(Line, File, Module, C),
|
|
build(Line, File, Module),
|
|
|
|
try
|
|
Result = eval_form(Line, Module, Block, Vars, S),
|
|
{ Export, Private, Def, Defmacro, Defmacrop, Functions } = elixir_def:unwrap_stored_definitions(Module),
|
|
|
|
{ All, Forms0 } = functions_form(Line, File, Module, Export, Private, Def, Defmacro, Defmacrop, Functions, C),
|
|
Forms1 = attributes_form(Line, File, Module, Forms0),
|
|
Forms2 = specs_form(Line, Module, Defmacro, Defmacrop, Forms1),
|
|
|
|
elixir_import:ensure_no_local_conflict(Line, File, Module, All),
|
|
elixir_import:ensure_no_import_conflict(Line, File, Module, All),
|
|
|
|
Final = [
|
|
{attribute, Line, file, {File,Line}},
|
|
{attribute, Line, module, Module} | Forms2
|
|
],
|
|
|
|
Binary = load_form(Line, Final, S),
|
|
{ module, Module, Binary, Result }
|
|
after
|
|
ets:delete(data_table(Module)),
|
|
ets:delete(docs_table(Module)),
|
|
ets:delete(spec_table(Module)),
|
|
elixir_def:delete_table(Module),
|
|
elixir_import:delete_table(Module)
|
|
end;
|
|
|
|
compile(Line, Other, _Block, _Vars, #elixir_scope{file=File}) ->
|
|
elixir_errors:form_error(Line, File, ?MODULE, { invalid_module, Other });
|
|
|
|
compile(Line, Module, Block, Vars, RawS) ->
|
|
Dict = [{ X, Y } || { X, Y, _ } <- Vars],
|
|
S = elixir_scope:deserialize(RawS, Dict),
|
|
compile(Line, Module, Block, Vars, S).
|
|
|
|
%% Hook that builds both attribute and functions and set up common hooks.
|
|
|
|
build(Line, File, Module) ->
|
|
%% Table with meta information about the module.
|
|
DataTable = data_table(Module),
|
|
|
|
case ets:info(DataTable, name) == DataTable of
|
|
true -> elixir_errors:form_error(Line, File, ?MODULE, { module_in_definition, Module });
|
|
false -> []
|
|
end,
|
|
|
|
ets:new(DataTable, [set, named_table, public]),
|
|
ets:insert(DataTable, { '__overridable', [] }),
|
|
ets:insert(DataTable, { before_compile, [] }),
|
|
ets:insert(DataTable, { after_compile, [] }),
|
|
|
|
case elixir_compiler:get_opt(docs) of
|
|
true -> ets:insert(DataTable, { on_definition, [{ 'Elixir.Module', compile_doc }] });
|
|
_ -> ets:insert(DataTable, { on_definition, [] })
|
|
end,
|
|
|
|
Attributes = [behavior, behaviour, on_load, spec, type, export_type, opaque, callback, compile],
|
|
ets:insert(DataTable, { '__acc_attributes', [before_compile,after_compile,on_definition|Attributes] }),
|
|
ets:insert(DataTable, { '__persisted_attributes', [vsn|Attributes] }),
|
|
|
|
%% Keep docs in another table since we don't want to pull out
|
|
%% all the binaries every time a new documentation is stored.
|
|
DocsTable = docs_table(Module),
|
|
ets:new(DocsTable, [ordered_set, named_table, public]),
|
|
|
|
%% Holds specs in a format it is easier to query.
|
|
SpecTable = spec_table(Module),
|
|
ets:new(SpecTable, [duplicate_bag, named_table, public]),
|
|
|
|
%% We keep a separated table for function definitions
|
|
%% and another one for imports. We keep them in different
|
|
%% tables for organization and speed purpose (since the
|
|
%% imports table is frequently written to).
|
|
elixir_def:build_table(Module),
|
|
elixir_import:build_table(Module).
|
|
|
|
%% Receives the module representation and evaluates it.
|
|
|
|
eval_form(Line, Module, Block, Vars, RawS) ->
|
|
Temp = ?ELIXIR_ATOM_CONCAT(["COMPILE-",Module]),
|
|
S = scope_for_eval(Module, RawS),
|
|
{ Value, NewS } = elixir_compiler:eval_forms([Block], Line, Temp, Vars, S),
|
|
elixir_def_overridable:store_pending(Module),
|
|
eval_callbacks(Line, Module, before_compile, [Module], NewS),
|
|
Value.
|
|
|
|
%% Return the form with exports and function declarations.
|
|
functions_form(Line, File, Module, Export, Private, Def, Defmacro, Defmacrop, Functions, C) ->
|
|
{ FinalExport, FinalFunctions } =
|
|
add_info_function(Line, File, Module, Export, Functions, Def, Defmacro, C),
|
|
|
|
Recorded = elixir_import:recorded_locals(Module),
|
|
elixir_def_local:check_unused_local_macros(File, Recorded, Defmacrop),
|
|
|
|
{ FinalExport ++ Private, [
|
|
{attribute, Line, export, lists:sort(FinalExport)} | FinalFunctions
|
|
] }.
|
|
|
|
%% Add attributes handling to the form
|
|
|
|
attributes_form(Line, _File, Module, Current) ->
|
|
Table = data_table(Module),
|
|
|
|
AccAttrs = ets:lookup_element(Table, '__acc_attributes', 2),
|
|
PersistedAttrs = ets:lookup_element(Table, '__persisted_attributes', 2),
|
|
|
|
Transform = fun({ Key, Value }, Acc) ->
|
|
case lists:member(Key, PersistedAttrs) of
|
|
false -> Acc;
|
|
true ->
|
|
Attrs = case lists:member(Key, AccAttrs) of
|
|
true -> Value;
|
|
false -> [Value]
|
|
end,
|
|
lists:foldl(fun(X, Final) -> [{ attribute, Line, Key, X }|Final] end, Acc, Attrs)
|
|
end
|
|
end,
|
|
|
|
ets:foldl(Transform, Current, Table).
|
|
|
|
%% Specs
|
|
|
|
specs_form(Line, Module, Defmacro, Defmacrop, Forms) ->
|
|
{ Specs, Callbacks } = ets:foldl(fun(X, Acc) ->
|
|
translate_spec(X, Defmacro, Defmacrop, Acc)
|
|
end, { [], [] }, spec_table(Module)),
|
|
|
|
Temp = specs_attributes(Line, spec, Forms, Specs),
|
|
specs_attributes(Line, callback, Temp, Callbacks).
|
|
|
|
specs_attributes(Line, Type, Forms, Specs) ->
|
|
Keys = lists:ukeysort(1, Specs),
|
|
lists:foldl(fun({ Tuple, _ }, Acc) ->
|
|
[{ attribute, Line, Type, { Tuple, proplists:append_values(Tuple, Specs) } }|Acc]
|
|
end, Forms, Keys).
|
|
|
|
translate_spec({ { spec, Spec }, Rest }, Defmacro, Defmacrop, { Specs, Callbacks } = Acc) ->
|
|
case ordsets:is_element(Spec, Defmacrop) of
|
|
true -> Acc;
|
|
false ->
|
|
case ordsets:is_element(Spec, Defmacro) of
|
|
true ->
|
|
{ Name, Arity } = Spec,
|
|
New = { { ?ELIXIR_MACRO(Name), Arity + 1 }, [spec_for_macro(X) || X <- Rest] },
|
|
{ [New|Specs], Callbacks };
|
|
false ->
|
|
{ [{ Spec, Rest }|Specs], Callbacks }
|
|
end
|
|
end;
|
|
|
|
translate_spec({ { callback, Spec }, Rest }, _, _, { Specs, Callbacks }) ->
|
|
{ Specs, [{ Spec, Rest }|Callbacks] }.
|
|
|
|
spec_for_macro({ type, Line, 'fun', [{ type, _, product, Args }|T] }) ->
|
|
NewArgs = [{type,Line,term,[]}|Args],
|
|
{ type, Line, 'fun', [{ type, Line, product, NewArgs }|T] };
|
|
|
|
spec_for_macro(Else) -> Else.
|
|
|
|
%% Loads the form into the code server.
|
|
|
|
load_form(Line, Forms, S) ->
|
|
elixir_compiler:module(Forms, S, fun(Module, Binary) ->
|
|
EvalS = scope_for_eval(Module, S),
|
|
eval_callbacks(Line, Module, after_compile, [Module, Binary], EvalS),
|
|
|
|
case get(elixir_compiled) of
|
|
Current when is_list(Current) ->
|
|
put(elixir_compiled, [{Module,Binary}|Current]),
|
|
case get(elixir_compiler_pid) of
|
|
undefined -> [];
|
|
PID -> PID ! { module_available, self(), Module, Binary }
|
|
end;
|
|
_ -> []
|
|
end,
|
|
|
|
Binary
|
|
end).
|
|
|
|
check_module_availability(Line, File, Module, Compiler) ->
|
|
case elixir_compiler:get_opt(ignore_module_conflict, Compiler) of
|
|
false ->
|
|
case code:ensure_loaded(Module) of
|
|
{ module, _ } ->
|
|
elixir_errors:handle_file_warning(File, { Line, ?MODULE, { module_defined, Module } });
|
|
{ error, _ } ->
|
|
[]
|
|
end;
|
|
true ->
|
|
[]
|
|
end.
|
|
|
|
% EXTRA FUNCTIONS
|
|
|
|
add_info_function(Line, File, Module, Export, Functions, Def, Defmacro, C) ->
|
|
Pair = { '__info__', 1 },
|
|
case lists:member(Pair, Export) of
|
|
true -> elixir_errors:form_error(Line, File, ?MODULE, {internal_function_overridden, Pair});
|
|
false ->
|
|
Docs = elixir_compiler:get_opt(docs, C),
|
|
Contents = { function, Line, '__info__', 1, [
|
|
functions_clause(Line, Def),
|
|
macros_clause(Line, Module, Def, Defmacro),
|
|
docs_clause(Line, Module, Docs),
|
|
moduledoc_clause(Line, Module, Docs),
|
|
compile_clause(Line),
|
|
self_clause(Line, Module),
|
|
else_clause(Line)
|
|
] },
|
|
{ [Pair|Export], [Contents|Functions] }
|
|
end.
|
|
|
|
functions_clause(Line, Def) ->
|
|
All = ordsets:add_element({'__info__',1}, Def),
|
|
{ clause, Line, [{ atom, Line, functions }], [], [elixir_tree_helpers:abstract_syntax(All)] }.
|
|
|
|
macros_clause(Line, Module, Def, Defmacro) ->
|
|
All = handle_builtin_macros(Module, Def, Defmacro),
|
|
{ clause, Line, [{ atom, Line, macros }], [], [elixir_tree_helpers:abstract_syntax(All)] }.
|
|
|
|
handle_builtin_macros('Elixir.Kernel', Def, Defmacro) ->
|
|
ordsets:subtract(ordsets:union(Defmacro,
|
|
elixir_dispatch:in_erlang_macros()), Def);
|
|
handle_builtin_macros(_, _Def, Defmacro) -> Defmacro.
|
|
|
|
self_clause(Line, Module) ->
|
|
{ clause, Line, [{ atom, Line, self }], [], [{ atom, Line, Module }] }.
|
|
|
|
docs_clause(Line, Module, true) ->
|
|
Docs = ordsets:from_list(ets:tab2list(docs_table(Module))),
|
|
{ clause, Line, [{ atom, Line, docs }], [], [elixir_tree_helpers:abstract_syntax(Docs)] };
|
|
|
|
docs_clause(Line, _Module, _) ->
|
|
{ clause, Line, [{ atom, Line, docs }], [], [{ atom, Line, nil }] }.
|
|
|
|
moduledoc_clause(Line, Module, true) ->
|
|
Docs = 'Elixir.Module':get_attribute(Module, moduledoc),
|
|
{ clause, Line, [{ atom, Line, moduledoc }], [], [elixir_tree_helpers:abstract_syntax({ Line, Docs })] };
|
|
|
|
moduledoc_clause(Line, _Module, _) ->
|
|
{ clause, Line, [{ atom, Line, moduledoc }], [], [{ atom, Line, nil }] }.
|
|
|
|
compile_clause(Line) ->
|
|
Info = { call, Line, { atom, Line, module_info }, [{ atom, Line, compile }] },
|
|
WrappedInfo = ?ELIXIR_WRAP_CALL(Line, 'Elixir.Keyword', 'from_enum', [Info]),
|
|
{ clause, Line, [{ atom, Line, compile }], [], [WrappedInfo] }.
|
|
|
|
else_clause(Line) ->
|
|
Info = { call, Line, { atom, Line, module_info }, [{ var, Line, atom }] },
|
|
{ clause, Line, [{ var, Line, atom }], [], [Info] }.
|
|
|
|
% HELPERS
|
|
|
|
eval_callbacks(Line, Module, Name, Args, RawS) ->
|
|
S = RawS#elixir_scope{check_clauses=false},
|
|
Binding = binding_for_eval(Module, []),
|
|
Callbacks = lists:reverse(ets:lookup_element(data_table(Module), Name, 2)),
|
|
Requires = S#elixir_scope.requires,
|
|
|
|
lists:foreach(fun({M,F}) ->
|
|
Expr = { { '.', Line, [M,F] }, Line, Args },
|
|
Scope = case ordsets:is_element(M, Requires) of
|
|
true -> S;
|
|
false -> S#elixir_scope{requires=ordsets:add_element(M, Requires)}
|
|
end,
|
|
|
|
{ Tree, _ } = elixir_translator:translate_each(Expr, Scope),
|
|
|
|
try
|
|
erl_eval:exprs([Tree], Binding)
|
|
catch
|
|
Kind:Reason ->
|
|
Info = { M, F, Args, [{ file, binary_to_list(S#elixir_scope.file) }, { line, Line }] },
|
|
erlang:raise(Kind, Reason, [Info|erlang:get_stacktrace()])
|
|
end
|
|
end, Callbacks).
|
|
|
|
% ERROR HANDLING
|
|
|
|
format_error({ internal_function_overridden, { Name, Arity } }) ->
|
|
io_lib:format("function ~s/~B is internal and should not be overridden", [Name, Arity]);
|
|
|
|
format_error({ invalid_module, Module}) ->
|
|
io_lib:format("invalid module name: ~p", [Module]);
|
|
|
|
format_error({ module_defined, Module }) ->
|
|
io_lib:format("redefining module ~s", [elixir_errors:inspect(Module)]);
|
|
|
|
format_error({ module_in_definition, Module }) ->
|
|
io_lib:format("cannot define module ~s because it is currently being defined",
|
|
[elixir_errors:inspect(Module)]).
|