Current section

Files

Jump to
gradualizer src gradualizer_lib.erl
Raw

src/gradualizer_lib.erl

%% @private
-module(gradualizer_lib).
-export([merge_with/3, top_sort/1, get_type_definition/3,
pick_values/2, fold_ast/3, get_ast_children/1,
empty_tenv/0, create_tenv/3,
remove_pos_typed_record_field/1,
ensure_form_list/1]).
-export_type([graph/1, tenv/0]).
-type type() :: gradualizer_type:abstract_type().
%% Type environment, passed around while comparing compatible subtypes.
-type tenv() :: #{ module => module(),
types := #{{Ty :: atom(), arity()} => {Params :: [atom()], Body :: type()}},
records := #{Rec :: atom() => [typechecker:typed_record_field()]} }.
-include("gradualizer.hrl").
-include("typechecker.hrl").
%% Pattern macros
-define(type(T), {type, _, T, []}).
-define(type(T, A), {type, _, T, A}).
%% Number of space used when prettyprinting records for nesting
-define(PP_RECORD_NESTING_OFFSET, 2).
-spec merge_with(fun((_, _, _) -> _), map(), map()) -> map().
-if(?OTP_RELEASE >= 24).
merge_with(F, M1, M2) ->
maps:merge_with(F, M1, M2).
-else.
merge_with(F, M1, M2) ->
case maps:size(M1) < maps:size(M2) of
true ->
maps:fold(fun (K, V1, M) ->
maps:update_with(K, fun (V2) -> F(K, V1, V2) end, V1, M)
end, M2, M1);
false ->
maps:fold(fun (K, V2, M) ->
maps:update_with(K, fun (V1) -> F(K, V1, V2) end, V2, M)
end, M1, M2)
end.
-endif.
%% -- Topological sort
-type graph(Node) :: #{Node => [Node]}. %% List of incoming edges (dependencies).
%% Topologically sorted strongly-connected components of a graph.
-spec top_sort(graph(Node)) -> [{cyclic, [Node]} | {acyclic, Node}].
top_sort(Graph) ->
Trees = dfs(Graph, lists:reverse(postorder(dff(reverse_graph(Graph))))),
Decode = fun(T) ->
case postorder(T) of
[I] -> case lists:member(I, maps:get(I, Graph, [])) of
true -> {cyclic, [I]};
false -> {acyclic, I}
end;
Is -> {cyclic, Is}
end end,
lists:map(Decode, Trees).
%% Depth first spanning forest of a graph.
dff(Graph) ->
dfs(Graph, maps:keys(Graph)).
dfs(Graph, Vs) ->
{_, Trees} = dfs(Graph, #{}, Vs, []),
Trees.
dfs(_Graph, Visited, [], Trees) -> {Visited, lists:reverse(Trees)};
dfs(Graph, Visited, [V | Vs], Trees) ->
case maps:is_key(V, Visited) of
true -> dfs(Graph, Visited, Vs, Trees);
false ->
{Visited1, Tree} = dfs1(Graph, Visited#{ V => true }, V),
dfs(Graph, Visited1, Vs, [Tree | Trees])
end.
dfs1(Graph, Visited, V) ->
Ws = maps:get(V, Graph, []),
{Visited1, Trees} = dfs(Graph, Visited, Ws, []),
{Visited1, {V, Trees}}.
%% Post-order traversal of a tree/forest.
postorder(Tree = {_, _}) -> postorder([Tree]);
postorder(Trees) when is_list(Trees) -> postorder(Trees, []).
postorder([], Acc) -> Acc;
postorder([{V, Trees1} | Trees], Acc) ->
postorder(Trees1, [V | postorder(Trees, Acc)]).
from_edges(Is, Es) ->
lists:foldl(fun({I, J}, G) ->
maps:update_with(I, fun(Js) -> lists:umerge([J], Js) end, [J], G)
end, maps:from_list([ {I, []} || I <- Is ]), Es).
reverse_graph(G) ->
from_edges(maps:keys(G), [ {J, I} || {I, Js} <- maps:to_list(G), J <- Js ]).
%% Look up `UserTy' definition:
%% first in `gradualizer_db', then, if not found, in provided `Types' map.
%% `UserTy' is actually an unexported `gradualizer_type:af_user_defined_type()'.
-spec get_type_definition(UserTy, Env, Opts) -> {ok, Ty} | opaque | not_found when
UserTy :: type(),
Env :: typechecker:env(),
Opts :: [annotate_user_types],
Ty :: type().
get_type_definition({remote_type, _Anno, [{atom, _, Module}, {atom, _, Name}, Args]}, _Env, _Opts) ->
%% We matched out the single atom arguments, so only Args :: [type()] remains.
Args = ?assert_type(Args, [type()]),
gradualizer_db:get_type(Module, Name, Args);
get_type_definition({user_type, Anno, Name, Args}, Env, Opts) ->
%% Let's check if the type is a known remote type.
case typelib:get_module_from_annotation(Anno) of
{ok, Module} ->
gradualizer_db:get_type(Module, Name, Args);
none ->
%% Let's check if the type is defined in the context of this module.
case maps:get({Name, length(Args)}, maps:get(types, Env#env.tenv), not_found) of
{Params, Type0} ->
VarMap = maps:from_list(lists:zip(Params, Args)),
Type2 = case proplists:is_defined(annotate_user_types, Opts) of
true ->
Module = maps:get(module, Env#env.tenv),
Type1 = typelib:annotate_user_type(Module, Type0),
typelib:substitute_type_vars(Type1, VarMap);
false ->
typelib:substitute_type_vars(Type0, VarMap)
end,
{ok, Type2};
not_found ->
not_found
end
end.
%% Given a type `Ty', pick a value of that type.
%% Used in exhaustiveness checking to show an example value
%% which is not covered by the cases.
-define(remote_type(Name, Args, Anno), {remote_type, Anno, [_, {atom, _, Name}, Args]}).
-define(user_type(Name, Args, Anno), {user_type, Anno, Name, Args}).
-spec pick_values(Tys, Env) -> AbstractVal when
Tys :: [type()],
Env :: typechecker:env(),
AbstractVal :: [gradualizer_type:abstract_expr()].
pick_values(Tys, Env) ->
[ pick_value(Ty, Env) || Ty <- Tys ].
-spec pick_value(Ty, Env) -> AbstractVal when
Ty :: type(),
Env :: typechecker:env(),
AbstractVal :: gradualizer_type:abstract_expr().
pick_value(?type(integer), _Env) ->
{integer, erl_anno:new(0), 0};
pick_value(?type(char), _Env) ->
{char, erl_anno:new(0), $a};
pick_value(?type(non_neg_integer), _Env) ->
{integer, erl_anno:new(0), 0};
pick_value(?type(pos_integer), _Env) ->
{integer, erl_anno:new(0), 0};
pick_value(?type(neg_integer), _Env) ->
L = erl_anno:new(0),
{op, L, '-', {integer, L, 1}};
pick_value(?type(float), _Env) ->
L = erl_anno:new(0),
{op, L, '-', {float, L, 1.0}};
pick_value(?type(atom), _Env) ->
{atom, erl_anno:new(0), a};
pick_value({atom, _, A}, _Env) ->
{atom, erl_anno:new(0), A};
pick_value({ann_type, _, [_, Ty]}, Env) ->
Ty = ?assert_type(Ty, type()),
pick_value(Ty, Env);
pick_value(?type(union, [Ty|_]), Env) ->
pick_value(Ty, Env);
pick_value(?type(tuple, any), _Env) ->
{tuple, erl_anno:new(0), []};
pick_value(?type(tuple, Tys), Env) ->
{tuple, erl_anno:new(0), [pick_value(Ty, Env) || Ty <- ?assert_type(Tys, list())]};
pick_value(?type(record, [{atom, _, RecordName}]), _Env) ->
{record, erl_anno:new(0), RecordName, []};
pick_value(?type(record, [{atom, _, RecordName} | Tys]), Env) ->
MFields = [
{record_field, erl_anno:new(0), {atom, erl_anno:new(0), FieldName}, pick_value(Ty, Env)}
|| ?type(field_type, [{atom, _, FieldName}, Ty]) <- Tys
],
{record, erl_anno:new(0), RecordName, MFields};
pick_value(?type(map, Assocs), Env) ->
NewAssocs = [ {AssocTag, Anno, pick_value(KTy, Env), pick_value(VTy, Env)}
|| {type, Anno, AssocTag, [KTy, VTy]} <- ?assert_type(Assocs, list()) ],
{map, erl_anno:new(0), NewAssocs};
pick_value(?type(boolean), _Env) ->
{atom, erl_anno:new(0), false};
pick_value(?type(string), _Env) ->
{nil, erl_anno:new(0)};
pick_value(?type(list), _Env) ->
{nil, erl_anno:new(0)};
pick_value(?type(list,_), _Env) ->
{nil, erl_anno:new(0)};
pick_value(?type(nonempty_list, [Ty]), Env) ->
H = pick_value(Ty, Env),
{cons, erl_anno:new(0), H, {nil, erl_anno:new(0)}};
pick_value(?type(nil), _Env) ->
{nil, erl_anno:new(0)};
pick_value(?type(binary, [{integer, _, M}, {integer, _, _}]), _Env) when M > 0 ->
L = erl_anno:new(0),
{bin, L, [{bin_element, L, {integer, L, 0}, {integer, L, M}, default}]};
pick_value(?type(binary, _), _Env) ->
{bin, erl_anno:new(0), []};
%% The ?type(range) is a different case because the type range
%% ..information is not encoded as an abstract_type()
%% i.e. {type, Anno, range, [{integer, Anno2, Low}, {integer, Anno3, High}]}
pick_value(?type(range, [{_TagLo, _, neg_inf}, Hi = {_TagHi, _, _Hi}]), _Env) ->
%% pick_value(Hi, Env);
Hi;
pick_value(?type(range, [Lo = {_TagLo, _, _Lo}, {_TagHi, _, _Hi}]), _Env) ->
%% pick_value(Lo, Env).
Lo;
pick_value(?remote_type(_, _, _) = Type, Env) ->
pick_remote_or_user_type_value(Type, Env);
pick_value(?user_type(_, _, _) = Type, Env) ->
pick_remote_or_user_type_value(Type, Env).
pick_remote_or_user_type_value(Type, Env) ->
{Kind, Anno, Name, Args} = case Type of
?remote_type(Name1, Args1, Anno1) ->
{remote_type, Anno1, Name1, Args1};
?user_type(Anno1, Name1, Args1) ->
{user_type, Anno1, Name1, Args1}
end,
case get_type_definition(Type, Env, [annotate_user_types]) of
{ok, Ty} ->
pick_value(Ty, Env);
opaque ->
UniqueOpaque = list_to_atom("Opaque" ++ integer_to_list(erlang:unique_integer([positive]))),
{var, Anno, UniqueOpaque};
not_found ->
throw({undef, Kind, Anno, {Name, length(Args)}})
end.
%% ------------------------------------------------
%% Functions for working with abstract syntax trees
%% ------------------------------------------------
%% erl_parse:erl_parse_tree() is documented but not exported :-(
-type erl_parse_tree() :: erl_parse:abstract_clause()
| erl_parse:abstract_expr()
| erl_parse:abstract_form()
| erl_parse:abstract_type().
%% Folds a function over an Erlang abstract syntax tree. The fun is applied to
%% each node (tuple) in the AST, which is traversed in depth first order.
-spec fold_ast(Fun, AccIn, Ast) -> AccOut
when Fun :: fun((tuple(), Acc) -> Acc),
Ast :: erl_parse_tree() | [erl_parse_tree()],
AccIn :: Acc,
AccOut :: Acc.
fold_ast(Fun, AccIn, [X|Xs]) ->
Acc = fold_ast(Fun, AccIn, X),
fold_ast(Fun, Acc, Xs);
fold_ast(Fun, AccIn, Node) when is_tuple(Node) ->
Acc = Fun(Node, AccIn),
fold_ast(Fun, Acc, get_ast_children(Node));
fold_ast(_Fun, Acc, _LiteralEtc) ->
Acc.
%% Returns the children of an AST node
get_ast_children({clauses, Clauses}) ->
%% This one doesn't have an annotation
Clauses;
get_ast_children(Node) ->
[_Tag, _Anno | Children] = tuple_to_list(Node),
Children.
%% ------------------------------------------------
%% Type environment
%% ------------------------------------------------
-spec empty_tenv() -> tenv().
empty_tenv() ->
#{types => #{},
records => #{}}.
-spec create_tenv(_, _, _) -> tenv().
create_tenv(Module, TypeDefs, RecordDefs) when is_atom(Module) ->
TypeMap =
maps:from_list([begin
Id = {Name, length(Vars)},
Params = [VarName || {var, _, VarName} <- Vars],
{Id, {Params, typelib:remove_pos(Body)}}
end || {Name, Body, Vars} <- TypeDefs]),
RecordMap =
maps:from_list([{Name, [remove_pos_typed_record_field(
absform:normalize_record_field(Field))
|| Field <- Fields]}
|| {Name, Fields} <- RecordDefs]),
#{module => Module,
types => TypeMap,
records => RecordMap}.
%% Removes the position annotation from a list of record fields normalized using
%% absform:normalize_record_field/1.
%%
%% Note: The field name (atom) is sometimes used as a type.
remove_pos_typed_record_field({typed_record_field,
{record_field, _, Name, Default},
Type}) ->
{typed_record_field,
{record_field, 0, typelib:remove_pos(Name), Default},
typelib:remove_pos(Type)}.
ensure_form_list(List) when is_list(List) ->
List;
ensure_form_list(Other) ->
[Other].