%% -*- erlang-indent-level: 2 -*-
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%%-------------------------------------------------------------------
%%% File : dialyzer_codeserver.erl
%%% Author : Tobias Lindahl <[email protected]>
%%% Description :
%%%
%%% Created : 4 Apr 2005 by Tobias Lindahl <[email protected]>
%%%-------------------------------------------------------------------
-module(dialyzer_codeserver).
-export([delete/1,
store_temp_contracts/4,
give_away/2,
finalize_contracts/1,
finalize_exported_types/2,
finalize_records/1,
get_contracts/1,
get_callbacks/1,
get_exported_types/1,
extract_exported_types/1,
get_exports/1,
get_records_table/1,
extract_records/1,
get_next_core_label/1,
get_temp_contracts/2,
all_temp_modules/1,
store_contracts/4,
get_temp_exported_types/1,
get_temp_records_table/1,
lookup_temp_mod_records/2,
insert/3,
insert_exports/2,
insert_temp_exported_types/2,
insert_fun_meta_info/2,
is_exported/2,
lookup_mod_code/2,
lookup_mfa_code/2,
lookup_mfa_var_label/2,
lookup_mod_records/2,
lookup_mod_contracts/2,
lookup_mfa_contract/2,
lookup_meta_info/2,
new/0,
set_next_core_label/2,
store_temp_records/3,
translate_fake_file/3]).
-export_type([codeserver/0, fun_meta_info/0, contracts/0]).
-include("dialyzer.hrl").
%%--------------------------------------------------------------------
-type dict_ets() :: ets:tid().
-type map_ets() :: ets:tid().
-type set_ets() :: ets:tid().
-type types() :: erl_types:type_table().
-type contracts() :: #{mfa() => dialyzer_contracts:file_contract()}.
%% A property-list of data compiled from -compile and -dialyzer attributes.
-type meta_info() :: [{{'nowarn_function' | dial_warn_tag()},
'mod' | 'func'}].
-type fun_meta_info() :: [{mfa(), meta_info()}
| {module(), [dial_warn_tag()]}].
-record(codeserver, {next_core_label = 0 :: label(),
code :: dict_ets(),
exported_types :: 'clean' | set_ets(), % set(mfa())
records :: 'clean' | map_ets(),
contracts :: map_ets(),
callbacks :: map_ets(),
fun_meta_info :: dict_ets(), % {mfa(), meta_info()}
exports :: 'clean' | set_ets(), % set(mfa())
temp_exported_types :: 'clean' | set_ets(), % set(mfa())
temp_records :: 'clean' | map_ets(),
temp_contracts :: 'clean' | map_ets(),
temp_callbacks :: 'clean' | map_ets()
}).
-opaque codeserver() :: #codeserver{}.
%%--------------------------------------------------------------------
ets_dict_find(Key, Table) ->
try ets:lookup_element(Table, Key, 2) of
Val -> {ok, Val}
catch
_:_ -> error
end.
ets_map_store(Key, Element, Table) ->
true = ets:insert(Table, {Key, Element}),
Table.
ets_dict_to_dict(Table) ->
Fold = fun({Key,Value}, Dict) -> dict:store(Key, Value, Dict) end,
ets:foldl(Fold, dict:new(), Table).
ets_set_is_element(Key, Table) ->
ets:lookup(Table, Key) =/= [].
ets_set_insert_set(Set, Table) ->
ets_set_insert_list(sets:to_list(Set), Table).
ets_set_insert_list(List, Table) ->
true = ets:insert(Table, [{E} || E <- List]).
ets_set_to_set(Table) ->
Fold = fun({E}, Set) -> sets:add_element(E, Set) end,
ets:foldl(Fold, sets:new(), Table).
%%--------------------------------------------------------------------
-spec new() -> codeserver().
new() ->
CodeOptions = [compressed, public, {read_concurrency, true}],
Code = ets:new(dialyzer_codeserver_code, CodeOptions),
ReadOptions = [compressed, {read_concurrency, true}],
[Contracts, Callbacks, Records, ExportedTypes] =
[ets:new(Name, ReadOptions) ||
Name <- [dialyzer_codeserver_contracts,
dialyzer_codeserver_callbacks,
dialyzer_codeserver_records,
dialyzer_codeserver_exported_types]],
TempOptions = [public, {write_concurrency, true}],
[Exports, FunMetaInfo, TempExportedTypes, TempRecords, TempContracts,
TempCallbacks] =
[ets:new(Name, TempOptions) ||
Name <-
[dialyzer_codeserver_exports, dialyzer_codeserver_fun_meta_info,
dialyzer_codeserver_temp_exported_types,
dialyzer_codeserver_temp_records, dialyzer_codeserver_temp_contracts,
dialyzer_codeserver_temp_callbacks]],
#codeserver{code = Code,
exports = Exports,
fun_meta_info = FunMetaInfo,
exported_types = ExportedTypes,
records = Records,
contracts = Contracts,
callbacks = Callbacks,
temp_exported_types = TempExportedTypes,
temp_records = TempRecords,
temp_contracts = TempContracts,
temp_callbacks = TempCallbacks}.
-spec delete(codeserver()) -> 'ok'.
delete(CServer) ->
lists:foreach(fun(Table) -> true = ets:delete(Table) end, tables(CServer)).
-spec insert(atom(), cerl:c_module(), codeserver()) -> codeserver().
insert(Mod, ModCode, CS) ->
Name = cerl:module_name(ModCode),
Exports = cerl:module_exports(ModCode),
Attrs = cerl:module_attrs(ModCode),
Defs = cerl:module_defs(ModCode),
{Files, SmallDefs} = compress_file_anno(Defs),
As = cerl:get_ann(ModCode),
Funs =
[{{Mod, cerl:fname_id(Var), cerl:fname_arity(Var)},
Val, {Var, cerl_trees:get_label(Fun)}} || Val = {Var, Fun} <- SmallDefs],
Keys = [Key || {Key, _Value, _Label} <- Funs],
ModEntry = {Mod, {Name, Exports, Attrs, Keys, As}},
ModFileEntry = {{mod, Mod}, Files},
true = ets:insert(CS#codeserver.code, [ModEntry, ModFileEntry|Funs]),
CS.
-spec get_temp_exported_types(codeserver()) -> sets:set(mfa()).
get_temp_exported_types(#codeserver{temp_exported_types = TempExpTypes}) ->
ets_set_to_set(TempExpTypes).
-spec insert_temp_exported_types(sets:set(mfa()), codeserver()) -> codeserver().
insert_temp_exported_types(Set, CS) ->
TempExportedTypes = CS#codeserver.temp_exported_types,
true = ets_set_insert_set(Set, TempExportedTypes),
CS.
-spec insert_exports([mfa()], codeserver()) -> codeserver().
insert_exports(List, #codeserver{exports = Exports} = CS) ->
true = ets_set_insert_list(List, Exports),
CS.
-spec insert_fun_meta_info(fun_meta_info(), codeserver()) -> codeserver().
insert_fun_meta_info(List, #codeserver{fun_meta_info = FunMetaInfo} = CS) ->
true = ets:insert(FunMetaInfo, List),
CS.
-spec is_exported(mfa(), codeserver()) -> boolean().
is_exported(MFA, #codeserver{exports = Exports}) ->
ets_set_is_element(MFA, Exports).
-spec get_exported_types(codeserver()) -> sets:set(mfa()).
get_exported_types(#codeserver{exported_types = ExpTypes}) ->
ets_set_to_set(ExpTypes).
-spec extract_exported_types(codeserver()) -> {codeserver(), set_ets()}.
extract_exported_types(#codeserver{exported_types = ExpTypes} = CS) ->
{CS#codeserver{exported_types = 'clean'}, ExpTypes}.
-spec get_exports(codeserver()) -> sets:set(mfa()).
get_exports(#codeserver{exports = Exports}) ->
ets_set_to_set(Exports).
-spec finalize_exported_types(sets:set(mfa()), codeserver()) -> codeserver().
finalize_exported_types(Set,
#codeserver{exported_types = ExportedTypes,
temp_exported_types = TempETypes} = CS) ->
true = ets_set_insert_set(Set, ExportedTypes),
true = ets:delete(TempETypes),
CS#codeserver{temp_exported_types = clean}.
-spec lookup_mod_code(atom(), codeserver()) -> cerl:c_module().
lookup_mod_code(Mod, CS) when is_atom(Mod) ->
table__lookup(CS#codeserver.code, Mod).
-spec lookup_mfa_code(mfa(), codeserver()) -> {cerl:c_var(), cerl:c_fun()}.
lookup_mfa_code({_M, _F, _A} = MFA, CS) ->
table__lookup(CS#codeserver.code, MFA).
-spec lookup_mfa_var_label(mfa(), codeserver()) -> {cerl:c_var(), label()}.
lookup_mfa_var_label({_M, _F, _A} = MFA, CS) ->
ets:lookup_element(CS#codeserver.code, MFA, 3).
-spec get_next_core_label(codeserver()) -> label().
get_next_core_label(#codeserver{next_core_label = NCL}) ->
NCL.
-spec set_next_core_label(label(), codeserver()) -> codeserver().
set_next_core_label(NCL, CS) ->
CS#codeserver{next_core_label = NCL}.
-spec lookup_mod_records(atom(), codeserver()) -> types().
lookup_mod_records(Mod, #codeserver{records = RecDict}) when is_atom(Mod) ->
case ets_dict_find(Mod, RecDict) of
error -> maps:new();
{ok, Map} -> Map
end.
-spec get_records_table(codeserver()) -> map_ets().
get_records_table(#codeserver{records = RecDict}) ->
RecDict.
-spec extract_records(codeserver()) -> {codeserver(), map_ets()}.
extract_records(#codeserver{records = RecDict} = CS) ->
{CS#codeserver{records = clean}, RecDict}.
-spec store_temp_records(module(), types(), codeserver()) -> codeserver().
store_temp_records(Mod, Map, #codeserver{temp_records = TempRecDict} = CS)
when is_atom(Mod) ->
case maps:size(Map) =:= 0 of
true -> CS;
false -> CS#codeserver{temp_records = ets_map_store(Mod, Map, TempRecDict)}
end.
-spec get_temp_records_table(codeserver()) -> map_ets().
get_temp_records_table(#codeserver{temp_records = TempRecDict}) ->
TempRecDict.
-spec lookup_temp_mod_records(module(), codeserver()) -> types().
lookup_temp_mod_records(Mod, #codeserver{temp_records = TempRecDict}) ->
case ets_dict_find(Mod, TempRecDict) of
error -> maps:new();
{ok, Map} -> Map
end.
-spec finalize_records(codeserver()) -> codeserver().
finalize_records(#codeserver{temp_records = TmpRecords,
records = Records} = CS) ->
%% The annotations of the abstract code are reset as they are no
%% longer needed, which makes the ETS table compression better.
A0 = erl_anno:new(0),
AFun = fun(_) -> A0 end,
FFun = fun({F, Abs, Type}) ->
NewAbs = erl_parse:map_anno(AFun, Abs),
{F, NewAbs, Type}
end,
ArFun = fun({Arity, Fields}) -> {Arity, lists:map(FFun, Fields)} end,
List = dialyzer_utils:ets_tab2list(TmpRecords),
true = ets:delete(TmpRecords),
Fun = fun({Mod, Map}) ->
MFun =
fun({record, _}, {FileLine, ArityFields}) ->
{FileLine, lists:map(ArFun, ArityFields)};
(_, {{M, FileLine, Abs, Args}, Type}) ->
{{M, FileLine, erl_parse:map_anno(AFun, Abs), Args}, Type}
end,
{Mod, maps:map(MFun, Map)}
end,
NewList = lists:map(Fun, List),
true = ets:insert(Records, NewList),
CS#codeserver{temp_records = clean}.
-spec lookup_mod_contracts(atom(), codeserver()) -> contracts().
lookup_mod_contracts(Mod, #codeserver{contracts = ContDict})
when is_atom(Mod) ->
case ets_dict_find(Mod, ContDict) of
error -> maps:new();
{ok, Keys} ->
maps:from_list([get_file_contract(Key, ContDict)|| Key <- Keys])
end.
get_file_contract(Key, ContDict) ->
{Key, ets:lookup_element(ContDict, Key, 2)}.
-spec lookup_mfa_contract(mfa(), codeserver()) ->
'error' | {'ok', dialyzer_contracts:file_contract()}.
lookup_mfa_contract(MFA, #codeserver{contracts = ContDict}) ->
ets_dict_find(MFA, ContDict).
-spec lookup_meta_info(module() | mfa(), codeserver()) -> meta_info().
lookup_meta_info(MorMFA, #codeserver{fun_meta_info = FunMetaInfo}) ->
case ets_dict_find(MorMFA, FunMetaInfo) of
error -> [];
{ok, PropList} -> PropList
end.
-spec get_contracts(codeserver()) ->
dict:dict(mfa(), dialyzer_contracts:file_contract()).
get_contracts(#codeserver{contracts = ContDict}) ->
dict:filter(fun({_M, _F, _A}, _) -> true;
(_, _) -> false
end, ets_dict_to_dict(ContDict)).
-spec get_callbacks(codeserver()) -> list().
get_callbacks(#codeserver{callbacks = CallbDict}) ->
ets:tab2list(CallbDict).
-spec store_temp_contracts(module(), contracts(), contracts(), codeserver()) ->
codeserver().
store_temp_contracts(Mod, SpecMap, CallbackMap,
#codeserver{temp_contracts = Cn,
temp_callbacks = Cb} = CS)
when is_atom(Mod) ->
%% Make sure Mod is stored even if there are no callbacks or
%% contracts.
CS1 = CS#codeserver{temp_contracts = ets_map_store(Mod, SpecMap, Cn)},
CS1#codeserver{temp_callbacks = ets_map_store(Mod, CallbackMap, Cb)}.
-spec all_temp_modules(codeserver()) -> [module()].
all_temp_modules(#codeserver{temp_contracts = TempContTable}) ->
ets:select(TempContTable, [{{'$1', '$2'}, [], ['$1']}]).
-spec store_contracts(module(), contracts(), contracts(), codeserver()) ->
codeserver().
store_contracts(Mod, SpecMap, CallbackMap, CS) ->
#codeserver{contracts = SpecDict, callbacks = CallbackDict} = CS,
Keys = maps:keys(SpecMap),
true = ets:insert(SpecDict, maps:to_list(SpecMap)),
true = ets:insert(SpecDict, {Mod, Keys}),
true = ets:insert(CallbackDict, maps:to_list(CallbackMap)),
CS.
-spec get_temp_contracts(module(), codeserver()) ->
{contracts(), contracts()}.
get_temp_contracts(Mod, #codeserver{temp_contracts = TempContDict,
temp_callbacks = TempCallDict}) ->
[{Mod, Contracts}] = ets:lookup(TempContDict, Mod),
true = ets:delete(TempContDict, Mod),
[{Mod, Callbacks}] = ets:lookup(TempCallDict, Mod),
true = ets:delete(TempCallDict, Mod),
{Contracts, Callbacks}.
-spec give_away(codeserver(), pid()) -> 'ok'.
give_away(CServer, Pid) ->
lists:foreach(fun(Table) -> true = ets:give_away(Table, Pid, any)
end, tables(CServer)).
tables(#codeserver{code = Code,
fun_meta_info = FunMetaInfo,
exports = Exports,
temp_exported_types = TempExpTypes,
temp_records = TempRecords,
temp_contracts = TempContracts,
temp_callbacks = TempCallbacks,
exported_types = ExportedTypes,
records = Records,
contracts = Contracts,
callbacks = Callbacks}) ->
[Table || Table <- [Code, FunMetaInfo, Exports, TempExpTypes,
TempRecords, TempContracts, TempCallbacks,
ExportedTypes, Records, Contracts, Callbacks],
Table =/= clean].
-spec finalize_contracts(codeserver()) -> codeserver().
finalize_contracts(#codeserver{temp_contracts = TempContDict,
temp_callbacks = TempCallDict} = CS) ->
true = ets:delete(TempContDict),
true = ets:delete(TempCallDict),
CS#codeserver{temp_contracts = clean, temp_callbacks = clean}.
-spec translate_fake_file(codeserver(), module(), file:filename()) ->
file:filename().
translate_fake_file(#codeserver{code = Code}, Module, FakeFile) ->
Files = ets:lookup_element(Code, {mod, Module}, 2),
{FakeFile, File} = lists:keyfind(FakeFile, 1, Files),
File.
table__lookup(TablePid, M) when is_atom(M) ->
{Name, Exports, Attrs, Keys, As} = ets:lookup_element(TablePid, M, 2),
Defs = [table__lookup(TablePid, Key) || Key <- Keys],
cerl:ann_c_module(As, Name, Exports, Attrs, Defs);
table__lookup(TablePid, MFA) ->
ets:lookup_element(TablePid, MFA, 2).
compress_file_anno(Term) ->
{Files, SmallTerm} = compress_file_anno(Term, []),
{[{FakeFile, File} || {File, {file, FakeFile}} <- Files], SmallTerm}.
compress_file_anno({file, F}, Fs) when is_list(F) ->
case lists:keyfind(F, 1, Fs) of
false ->
I = integer_to_list(length(Fs)),
FileI = {file, I},
NFs = [{F, FileI}|Fs],
{NFs, FileI};
{F, FileI} -> {Fs, FileI}
end;
compress_file_anno(T, Fs) when is_tuple(T) ->
{NFs, NL} = compress_file_anno(tuple_to_list(T), Fs),
{NFs, list_to_tuple(NL)};
compress_file_anno([E|L], Fs) ->
{Fs1, NE} = compress_file_anno(E, Fs),
{NFs, NL} = compress_file_anno(L, Fs1),
{NFs, [NE|NL]};
compress_file_anno(T, Fs) -> {Fs, T}.