Files
n8n-openai-adapter/lib/elixir/src/elixir_module.erl
T
José Valim cbd9d83adc Ensure compilation does not leave random spurious data on failure
Prior to this patch, the parallel compiler could leave spurious
processes and the module definition could leave stale data
in the elixir_modules table.

The issue has been fixed by emitting the proper exit signals
and by using monitors.
2014-11-21 17:40:06 +01:00

508 lines
16 KiB
Erlang

-module(elixir_module).
-export([data_table/1, defs_table/1, clas_table/1, is_open/1,
compile/4, expand_callback/6, add_beam_chunk/3, format_error/1]).
-include("elixir.hrl").
-define(acc_attr, {elixir, acc_attributes}).
-define(lexical_attr, {elixir, lexical_tracker}).
-define(persisted_attr, {elixir, persisted_attributes}).
-define(overridable_attr, {elixir, overridable}).
-define(location_attr, {elixir, location}).
%% TABLE METHODS
data_table(Module) ->
ets:lookup_element(elixir_modules, Module, 2).
defs_table(Module) ->
ets:lookup_element(elixir_modules, Module, 3).
clas_table(Module) ->
ets:lookup_element(elixir_modules, Module, 4).
is_open(Module) ->
ets:lookup(elixir_modules, Module) /= [].
%% 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 ?m(Env, function) of
nil -> Env#{module := Module, local := nil};
_ -> Env#{lexical_tracker := nil, function := nil, module := Module, local := nil}
end,
case ?m(LexEnv, lexical_tracker) of
nil ->
elixir_lexical:run(?m(LexEnv, file), nil, fun(Pid) ->
do_compile(Line, Module, Block, Vars, LexEnv#{lexical_tracker := Pid})
end);
_ ->
do_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}).
do_compile(Line, Module, Block, Vars, E) ->
File = ?m(E, file),
check_module_availability(Line, File, Module),
Docs = elixir_compiler:get_opt(docs),
{Data, Defs, Clas, Ref} = build(Line, File, Module, Docs, ?m(E, lexical_tracker)),
try
{Result, NE} = eval_form(Line, Module, Data, Block, Vars, E),
_ = case ets:lookup(Data, 'on_load') of
[] -> ok;
[{on_load,OnLoad}] ->
[elixir_locals:record_local(Tuple, Module) || Tuple <- OnLoad]
end,
{Def, Defp, Defmacro, Defmacrop, Exports, Functions} =
elixir_def:unwrap_definitions(File, Module),
{All, Forms0} = functions_form(Line, File, Module, Def, Defp,
Defmacro, Defmacrop, Exports, Functions),
Forms1 = specs_form(Data, Defmacro, Defmacrop, Forms0),
Forms2 = types_form(Line, File, Data, Forms1),
Forms3 = attributes_form(Line, File, Data, Forms2),
elixir_locals:ensure_no_import_conflict(Line, File, Module, All),
case Docs of
true -> warn_unused_docs(Line, File, Data, doc);
false -> false
end,
Location = {elixir_utils:characters_to_list(elixir_utils:relative_to_cwd(File)), Line},
Final = [
{attribute, Line, file, Location},
{attribute, Line, module, Module} | Forms3
],
Binary = load_form(Line, Data, Final, compile_opts(Module), NE),
{module, Module, Binary, Result}
after
elixir_locals:cleanup(Module),
ets:delete(Data),
ets:delete(Defs),
ets:delete(Clas),
elixir_code_server:call({undefmodule, Ref})
end.
%% Hook that builds both attribute and functions and set up common hooks.
build(Line, File, Module, Docs, Lexical) ->
case ets:lookup(elixir_modules, 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, [set, public]),
Clas = ets:new(Module, [bag, public]),
Ref = elixir_code_server:call({defmodule, self(),
{Module, Data, Defs, Clas, Line, File}}),
ets:insert(Data, {before_compile, []}),
ets:insert(Data, {after_compile, []}),
ets:insert(Data, {moduledoc, nil}),
case Docs of
true -> ets:insert(Data, {on_definition, [{'Elixir.Module', compile_doc}]});
_ -> ets:insert(Data, {on_definition, []})
end,
Attributes = [behaviour, on_load, compile, external_resource],
ets:insert(Data, {?acc_attr, [before_compile, after_compile, on_definition, derive,
spec, type, typep, opaque, callback|Attributes]}),
ets:insert(Data, {?persisted_attr, [vsn|Attributes]}),
ets:insert(Data, {?lexical_attr, Lexical}),
%% Setup definition related modules
elixir_def:setup(Module),
elixir_locals:setup(Module),
elixir_def_overridable:setup(Module),
{Data, Defs, Clas, Ref}.
%% Receives the module representation and evaluates it.
eval_form(Line, Module, Data, Block, Vars, E) ->
{Value, EE} = elixir_compiler:eval_forms(Block, Vars, E),
elixir_def_overridable:store_pending(Module),
EV = elixir_env:linify({Line, EE#{vars := [], export_vars := nil}}),
EC = eval_callbacks(Line, Data, before_compile, [EV], EV),
elixir_def_overridable:store_pending(Module),
{Value, EC}.
eval_callbacks(Line, Data, Name, Args, E) ->
Callbacks = lists:reverse(ets:lookup_element(Data, Name, 2)),
lists:foldl(fun({M,F}, Acc) ->
expand_callback(Line, M, F, Args, Acc#{vars := [], export_vars := nil},
fun(AM, AF, AA) -> apply(AM, AF, AA) end)
end, E, Callbacks).
%% Return the form with exports and function declarations.
functions_form(Line, File, Module, Def, Defp, Defmacro, Defmacrop, Exports, Body) ->
All = Def ++ Defmacro ++ Defp ++ Defmacrop,
{Spec, Info} = add_info_function(Line, File, Module, All, Def, Defmacro),
{[{'__info__',1}|All],
[{attribute, Line, export, lists:sort([{'__info__',1}|Exports])},
Spec, Info | Body]}.
%% Add attributes handling to the form
attributes_form(Line, File, Data, Current) ->
AccAttrs = ets:lookup_element(Data, ?acc_attr, 2),
PersistedAttrs = ets:lookup_element(Data, ?persisted_attr, 2),
Transform = fun({Key, Value}, Acc) when is_atom(Key) ->
case lists:member(Key, PersistedAttrs) of
false -> Acc;
true ->
Values =
case lists:member(Key, AccAttrs) of
true -> Value;
false -> [Value]
end,
lists:foldl(fun(X, Final) ->
[{attribute, Line, Key, X}|Final]
end, Acc, process_attribute(Line, File, Key, Values))
end
end,
Results = ets:select(Data, [{{'$1', '_'}, [{is_atom, '$1'}], ['$_']}]),
lists:foldl(Transform, Current, Results).
process_attribute(Line, File, external_resource, Values) ->
lists:usort([process_external_resource(Line, File, Value) || Value <- Values]);
process_attribute(_Line, _File, _Key, Values) ->
Values.
process_external_resource(_Line, _File, Value) when is_binary(Value) ->
Value;
process_external_resource(Line, File, Value) ->
elixir_errors:form_error([{line, Line}], File,
?MODULE, {invalid_external_resource, Value}).
%% Types
types_form(Line, File, Data, Forms0) ->
case code:ensure_loaded('Elixir.Kernel.Typespec') of
{module, 'Elixir.Kernel.Typespec'} ->
Types0 = get_typespec(Data, type) ++ get_typespec(Data, typep)
++ get_typespec(Data, opaque),
Types1 = ['Elixir.Kernel.Typespec':translate_type(Kind, Expr, Doc, Caller) ||
{Kind, Expr, Doc, Caller} <- Types0],
warn_unused_docs(Line, File, Data, typedoc),
Forms1 = types_attributes(Types1, Forms0),
Forms2 = export_types_attributes(Types1, Forms1),
typedocs_attributes(Types1, Forms2);
{error, _} ->
Forms0
end.
types_attributes(Types, Forms) ->
Fun = fun({{Kind, _NameArity, Expr}, Line, _Export, _Doc}, Acc) ->
[{attribute, Line, Kind, Expr}|Acc]
end,
lists:foldl(Fun, Forms, Types).
export_types_attributes(Types, Forms) ->
Fun = fun
({{_Kind, NameArity, _Expr}, Line, true, _Doc}, Acc) ->
[{attribute, Line, export_type, [NameArity]}|Acc];
({_Type, _Line, false, _Doc}, Acc) ->
Acc
end,
lists:foldl(Fun, Forms, Types).
typedocs_attributes(Types, Forms) ->
Fun = fun
({{_Kind, NameArity, _Expr}, Line, true, Doc}, Acc) when Doc =/= nil ->
[{attribute, Line, typedoc, {NameArity, Doc}}|Acc];
({_Type, _Line, _Export, _Doc}, Acc) ->
Acc
end,
lists:foldl(Fun, Forms, Types).
%% Specs
specs_form(Data, Defmacro, Defmacrop, Forms) ->
case code:ensure_loaded('Elixir.Kernel.Typespec') of
{module, 'Elixir.Kernel.Typespec'} ->
Specs0 = get_typespec(Data, spec) ++ get_typespec(Data, callback),
Specs1 = ['Elixir.Kernel.Typespec':translate_spec(Kind, Expr, Caller) ||
{Kind, Expr, Caller} <- Specs0],
Specs2 = lists:flatmap(fun(Spec) ->
translate_macro_spec(Spec, Defmacro, Defmacrop)
end, Specs1),
specs_attributes(Forms, Specs2);
{error, _} ->
Forms
end.
specs_attributes(Forms, Specs) ->
Dict = lists:foldl(fun({{Kind, NameArity, Spec}, Line}, Acc) ->
dict:append({Kind, NameArity}, {Spec, Line}, Acc)
end, dict:new(), Specs),
dict:fold(fun({Kind, NameArity}, ExprsLines, Acc) ->
{Exprs, Lines} = lists:unzip(ExprsLines),
Line = lists:min(Lines),
[{attribute, Line, Kind, {NameArity, Exprs}}|Acc]
end, Forms, Dict).
translate_macro_spec({{spec, NameArity, Spec}, Line}, Defmacro, Defmacrop) ->
case lists:member(NameArity, Defmacrop) of
true -> [];
false ->
case lists:member(NameArity, Defmacro) of
true ->
{Name, Arity} = NameArity,
[{{spec, {elixir_utils:macro_name(Name), Arity + 1}, spec_for_macro(Spec)}, Line}];
false ->
[{{spec, NameArity, Spec}, Line}]
end
end;
translate_macro_spec({{callback, NameArity, Spec}, Line}, _Defmacro, _Defmacrop) ->
[{{callback, NameArity, Spec}, Line}].
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.
compile_opts(Module) ->
case ets:lookup(data_table(Module), compile) of
[{compile,Opts}] when is_list(Opts) -> Opts;
[] -> []
end.
load_form(Line, Data, Forms, Opts, E) ->
elixir_compiler:module(Forms, Opts, E, fun(Module, Binary0) ->
Docs = elixir_compiler:get_opt(docs),
Binary = add_docs_chunk(Binary0, Data, Line, Docs),
eval_callbacks(Line, Data, after_compile, [E, Binary], E),
case get(elixir_compiled) of
Current when is_list(Current) ->
put(elixir_compiled, [{Module,Binary}|Current]),
case get(elixir_compiler_pid) of
undefined -> ok;
PID ->
Ref = make_ref(),
PID ! {module_available, self(), Ref, ?m(E, file), Module, Binary},
receive {Ref, ack} -> ok end
end;
_ ->
ok
end,
Binary
end).
add_docs_chunk(Bin, Data, Line, true) ->
ChunkData = term_to_binary({elixir_docs_v1, [
{docs, get_docs(Data)},
{moduledoc, get_moduledoc(Line, Data)}
]}),
add_beam_chunk(Bin, "ExDc", ChunkData);
add_docs_chunk(Bin, _, _, _) -> Bin.
get_docs(Data) ->
Match = ets:match(Data, {{doc, '$1'}, '$2', '$3', '$4', '$5'}),
lists:usort(
[{Tuple, Line, Kind, Sig, Doc} ||
[Tuple, Line, Kind, Sig, Doc] <- Match,
Kind =/= type, Kind =/= opaque]).
get_moduledoc(Line, Data) ->
{Line, ets:lookup_element(Data, moduledoc, 2)}.
get_typespec(Data, Key) ->
case ets:lookup(Data, Key) of
[{Key, Value}] -> Value;
[] -> []
end.
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.
warn_unused_docs(Line, File, Data, Attribute) ->
case ets:member(Data, Attribute) of
true ->
elixir_errors:form_warn([{line, Line}], File, ?MODULE, {unused_doc, Attribute});
_ ->
ok
end.
% __INFO__
add_info_function(Line, File, Module, All, Def, Defmacro) ->
Pair = {'__info__', 1},
case lists:member(Pair, All) of
true ->
elixir_errors:form_error([{line, Line}], File, ?MODULE, {internal_function_overridden, Pair});
false ->
Spec =
{attribute, Line, spec, {Pair,
[{type, Line, 'fun', [
{type, Line, product, [
{type, Line, union, [
{atom, Line, attributes},
{atom, Line, compile},
{atom, Line, exports},
{atom, Line, functions},
{atom, Line, imports},
{atom, Line, macros},
{atom, Line, module}
]}
]},
{type, Line, union, [
{type, Line, atom, []},
{type, Line, list, [
{type, Line, tuple, [
{type, Line, atom, []},
{type, Line, any, []}
]}
]}
]}
]}]
}},
Info =
{function, 0, '__info__', 1, [
functions_clause(Def),
macros_clause(Defmacro),
module_clause(Module),
else_clause()
]},
{Spec, Info}
end.
functions_clause(Def) ->
{clause, 0, [{atom, 0, functions}], [], [elixir_utils:elixir_to_erl(lists:sort(Def))]}.
macros_clause(Defmacro) ->
{clause, 0, [{atom, 0, macros}], [], [elixir_utils:elixir_to_erl(lists:sort(Defmacro))]}.
module_clause(Module) ->
{clause, 0, [{atom, 0, module}], [], [{atom, 0, Module}]}.
else_clause() ->
Info = {call, 0, {atom, 0, module_info}, [{var, 0, atom}]},
{clause, 0, [{var, 0, atom}], [], [Info]}.
% HELPERS
%% Adds custom chunk to a .beam binary
add_beam_chunk(Bin, Id, ChunkData)
when is_binary(Bin), is_list(Id), is_binary(ChunkData) ->
{ok, _, Chunks0} = beam_lib:all_chunks(Bin),
NewChunk = {Id, ChunkData},
Chunks = [NewChunk|Chunks0],
{ok, NewBin} = beam_lib:build_module(Chunks),
NewBin.
%% Expands a callback given by M:F(Args). In case
%% the callback can't be expanded, invokes the given
%% fun passing a possibly expanded AM:AF(Args).
expand_callback(Line, M, F, Args, E, Fun) ->
Meta = [{line,Line},{require,false}],
{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 ->
Info = {M, F, length(Args), location(Line, E)},
erlang:raise(Kind, Reason, prune_stacktrace(Info, erlang:get_stacktrace()))
end
end.
location(Line, E) ->
[{file, elixir_utils:characters_to_list(?m(E, file))}, {line, Line}].
%% 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].
% ERROR HANDLING
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_doc, typedoc}) ->
"@typedoc provided but no type follows it";
format_error({unused_doc, doc}) ->
"@doc provided but no definition follows it";
format_error({internal_function_overridden, {Name, Arity}}) ->
io_lib:format("function ~ts/~B is internal and should not be overridden", [Name, Arity]);
format_error({invalid_module, Module}) ->
io_lib:format("invalid module name: ~ts", ['Elixir.Kernel':inspect(Module)]);
format_error({module_defined, Module}) ->
io_lib:format("redefining module ~ts", [elixir_aliases:inspect(Module)]);
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.Path':relative_to_cwd(File), Line]).