Current section

Files

Jump to
gradualizer src typelib.erl
Raw

src/typelib.erl

%% @private
%% @doc
%% Functions operating on types represented as the Erlang Abstract Forms.
%% @end
-module(typelib).
-export([remove_pos/1,
annotate_user_type/2, annotate_user_types/2,
get_module_from_annotation/1,
substitute_type_vars/2,
pp_type/1, debug_type/3, parse_type/1,
reduce_type/3]).
-export_type([constraint/0, function_type/0, extended_type/0]).
-type af_constraint() :: gradualizer_type:af_constraint().
-type type() :: gradualizer_type:abstract_type().
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% Parsing and pretty printing types
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-include("gradualizer.hrl").
-type constraint() :: {type, erl_anno:anno(),
constraint,
{atom, erl_anno:anno(), is_subtype},
[{var, erl_anno:anno(), atom()} | type()]}.
-type function_type() :: {type, erl_anno:anno(),
'fun',
[{type, erl_anno:anno(), product, [type()]} |
type()]}.
-type extended_type() :: type() |
{type, erl_anno:anno(), bounded_fun,
[function_type() | [constraint()]]} |
[extended_type()].
%% @doc
%% Pretty-print a type represented as an Erlang abstract form.
%% @end
-spec pp_type(extended_type()) -> string().
pp_type(Types = [_|_]) ->
%% TODO: This is a workaround for the fact that a list is sometimes used in
%% place of a type. It typically represents a function type with multiple
%% clauses. We should perhaps represented them as a tuples on the form
%% {type, Anno, intersection, Types} instead.
lists:join("; ", lists:map(fun pp_type/1, Types));
pp_type({type, _, bounded_fun, [FunType, []]}) ->
%% Bounded fun with empty constraints gets printed with a trailing "when"
%% when pretty-printed as a spec (next clause)
pp_type(?assert_type(FunType, function_type()));
pp_type(Type = {type, _, bounded_fun, _}) ->
%% erl_pp can't handle bounded_fun in type definitions
%% We invent our own syntax here, e.g. "fun((A) -> ok when A :: atom())"
Form = {attribute, erl_anno:new(0), spec, {{foo, 0}, [Type]}},
TypeDef = erl_pp:form(Form),
{match, [S]} = re:run(TypeDef, <<"-spec foo\\s*(.*)\\.\\n*$">>,
[{capture, all_but_first, list}, dotall]),
"fun(" ++ S ++ ")";
pp_type({var, _, TyVar}) ->
%% See gradualizer_type:af_type_variable/0 and typechecker:new_type_var/0
if
is_atom(TyVar) -> atom_to_list(TyVar);
is_list(TyVar) -> TyVar
end;
pp_type(Type) ->
%% erl_pp can handle type definitions, so wrap Type in a type definition
%% and then take the type from that.
Form = {attribute, erl_anno:new(0), type, {t, Type, []}},
TypeDef = erl_pp:form(Form),
{match, [S]} = re:run(TypeDef, <<"::\\s*(.*)\\.\\n*">>,
[{capture, all_but_first, list}, dotall]),
case S of "INVALID" ++ _ -> error({badarg, Type});
_ -> ok end,
S.
%case erl_anno:file(element(2, Type)) of
% undefined -> S;
% File -> S ++ " in " ++ File
%end.
%% Looks up and prints the type M:N(P1, ..., Pn).
debug_type(M, N, P) ->
case gradualizer_db:get_type(M, N, P) of
{ok, T} ->
Params = lists:join($,, lists:map(fun pp_type/1, P)),
io:format("~w:~w(~s) :: ~s.~n",
[M, N, Params, pp_type(T)]);
not_found ->
not_found
end.
-spec parse_type(string()) -> type().
parse_type(Src) ->
AttrSrc = "-type t() :: " ++ Src ++ ".",
{ok, Tokens, _EndLocation} = erl_scan:string(AttrSrc),
{ok, {attribute, _, type, {t, Type, []}}} = erl_parse:parse_form(Tokens),
Type.
%% Removes all annotations from type, except filename in two cases: Filename is
%% kept for user-defined types and record types. Filename is used to
%% disambiguate between types with the same name from different modules.
%% Annotated types as in Name :: Type are also removed.
-type any_t_no_args() :: {type, erl_anno:anno(), any}.
-type unary_op() :: gradualizer_type:af_unary_op(_).
-type binary_op() :: gradualizer_type:af_binary_op(_).
-spec remove_pos(list()) -> list();
(any_t_no_args()) -> any_t_no_args();
(af_constraint()) -> af_constraint();
(type()) -> type();
(unary_op()) -> unary_op();
(binary_op()) -> binary_op().
remove_pos([]) ->
[];
remove_pos([_|_] = L) ->
lists:map(fun remove_pos/1, L);
remove_pos({type, _, any}) ->
%% special case for `fun((...) -> R)`,
%% the only place where `{type, _, any}` can occur
{type, erl_anno:new(0), any};
remove_pos({type, _, constraint, [{atom, _, is_subtype}, Args]}) ->
Args = ?assert_type(Args, [type()]),
L = erl_anno:new(0),
{type, L, constraint, [{atom, L, is_subtype}, lists:map(fun remove_pos/1, Args)]};
remove_pos({Type, _, Value})
when Type == atom; Type == integer; Type == char; Type == var ->
{Type, erl_anno:new(0), Value};
remove_pos({user_type, Anno, Name, Params}) when is_list(Params) ->
{user_type, anno_keep_only_filename(Anno), Name,
lists:map(fun remove_pos/1, Params)};
remove_pos({type, Anno, record, [Name | TypedFields]}) ->
{type, anno_keep_only_filename(Anno), record,
[remove_pos(Name)] ++ lists:map(fun remove_pos/1, TypedFields)};
remove_pos({type, _, field_type, [FName, FTy]}) ->
{type, erl_anno:new(0), field_type, [remove_pos(FName), remove_pos(FTy)]};
remove_pos({type, _, Type, Params}) when is_list(Params) ->
{type, erl_anno:new(0), Type, lists:map(fun remove_pos/1, Params)};
remove_pos({type, _, Type, any}) when Type == tuple; Type == map ->
{type, erl_anno:new(0), Type, any};
remove_pos({type, _, Assoc, Tys})
when Assoc == map_field_exact;
Assoc == map_field_assoc ->
{type, erl_anno:new(0), Assoc, lists:map(fun remove_pos/1, Tys)};
remove_pos({remote_type, _, [Mod, Name, Params]}) ->
Params = ?assert_type(Params, list()),
Params1 = lists:map(fun remove_pos/1, Params),
{remote_type, erl_anno:new(0), [Mod, Name, Params1]};
remove_pos({ann_type, _, [_Var, Type]}) ->
%% Also remove annotated types one the form Name :: Type
remove_pos(?assert_type(Type, type()));
remove_pos({op, _, Op, Type}) ->
{op, erl_anno:new(0), Op, remove_pos(Type)};
remove_pos({op, _, Op, Type1, Type2}) ->
{op, erl_anno:new(0), Op, remove_pos(Type1), remove_pos(Type2)}.
%% Helper for remove_pos/1. Removes all annotations except filename.
-spec anno_keep_only_filename(erl_anno:anno()) -> erl_anno:anno().
anno_keep_only_filename(Anno) ->
NewAnno = erl_anno:new(0),
case erl_anno:file(Anno) of
undefined -> NewAnno;
Filename -> erl_anno:set_file(Filename, NewAnno)
end.
%% Annotate a user-defined type or record type with a file name.
-spec annotate_user_type(module() | file:filename(), type()) -> type().
annotate_user_type(ModOrFile, Type) ->
Filename = ensure_filename(ModOrFile),
annotate_user_type_(Filename, Type).
-spec ensure_filename(module() | file:filename()) -> file:filename().
ensure_filename(ModOrFile) ->
case ModOrFile of
Module when is_atom(ModOrFile) ->
atom_to_list(?assert_type(Module, atom())) ++ ".erl";
_ -> ModOrFile
end.
%% Annotate user-defined types and record types with a file name.
-spec annotate_user_types(ModOrFile, TypeOrTypes) -> type() | [type()] when
ModOrFile :: module() | file:filename(),
TypeOrTypes :: type() | [type()].
annotate_user_types(ModOrFile, TypeOrTypes) ->
case TypeOrTypes of
Types when is_list(Types) ->
[ annotate_user_type(ModOrFile, Type) || Type <- ?assert_type(Types, [type()]) ];
Type ->
annotate_user_type(ModOrFile, ?assert_type(Type, type()))
end.
-spec annotate_user_type_(file:filename(), type()) -> type().
annotate_user_type_(Filename, {user_type, Anno, Name, Params}) ->
%% Annotate local user-defined type.
{user_type, erl_anno:set_file(Filename, Anno), Name,
[annotate_user_type_(Filename, Param) || Param <- Params]};
annotate_user_type_(Filename, {type, Anno, record, RecName = [_]}) ->
%% Annotate local record type
{type, erl_anno:set_file(Filename, Anno), record, RecName};
annotate_user_type_(Filename, {type, Anno, T, Params}) when is_list(Params) ->
{type, Anno, T, [ annotate_user_types(Filename, Param)
|| Param <- ?assert_type(Params, [type()]) ]};
annotate_user_type_(Filename, {ann_type, Anno, [Var, Type]}) ->
%% We match Var :: af_anno() and Type :: type() above.
Type = ?assert_type(Type, type()),
{ann_type, Anno, [Var, annotate_user_type_(Filename, Type)]};
annotate_user_type_(_Filename, Type) ->
Type.
-spec get_module_from_annotation(erl_anno:anno()) -> {ok, module()} | none.
get_module_from_annotation(Anno) ->
case erl_anno:file(Anno) of
File when is_list(File) ->
Basename = filename:basename(File, ".erl"),
{ok, list_to_existing_atom(?assert_type(Basename, string()))};
undefined ->
none
end.
-spec substitute_type_vars(type(),
#{atom() => type()}) -> type().
substitute_type_vars({type, L, 'fun', [Any = {type, _, any}, RetTy]}, TVars) ->
%% Special case for `fun((...) -> R)',
%% the only place where `{type, _, any}' can occur.
%% We match on `{type, _, any}' in the head explicitly, so `RetTy' cannot contain it - the
%% assertion is safe.
RetTy = ?assert_type(RetTy, type()),
{type, L, 'fun', [Any, substitute_type_vars(RetTy, TVars)]};
substitute_type_vars({Tag, L, T, Params}, TVars)
when Tag == type orelse
Tag == user_type,
is_list(Params) ->
%% We have to assert the type below as we're running into the problem documented
%% with test/known_problems/should_pass/lc_cannot_glb_different_variants.erl.
%% In other words, the 4th element of a `type()' tuple doesn't have to be a list
%% and Gradualizer cannot yet use the `is_list(Params)' guard to refine the type.
{Tag, L, T, [substitute_type_vars(P, TVars) || P <- ?assert_type(Params, list())]};
substitute_type_vars({remote_type, L, [M, T, Params]}, TVars) ->
{remote_type, L, [M, T, [substitute_type_vars(P, TVars) || P <- Params]]};
substitute_type_vars({ann_type, L, [Var = {var, _, _}, Type]}, TVars) ->
%% We matched out Var :: af_anno() from [af_anno() | type()] above.
Type = ?assert_type(Type, type()),
{ann_type, L, [Var, substitute_type_vars(Type, TVars)]};
substitute_type_vars({var, L, Var}, TVars) ->
case TVars of
#{Var := Type} -> Type;
_ -> {var, L, Var}
end;
substitute_type_vars(Other = {type, _, T, any}, _)
when T == tuple; T == map ->
Other;
substitute_type_vars(Other = {op, _, _Op, _Arg}, _) ->
%% unary integer operator - cannot contain type vars
Other;
substitute_type_vars(Other = {op, _, _Op, _Arg1, _Arg2}, _) ->
%% binary integer operator - cannot contain type vars
Other;
substitute_type_vars(Other = {T, _, _}, _)
when T == atom; T == integer; T == char ->
Other.
-type walkable_type() :: gradualizer_type:abstract_type() | {type, _, any} | pos_inf | neg_inf.
%% `gradualizer_type:abstract_type()' defines the abstract representation of a type.
%% The type is a tree of nodes. However, there are more node kinds in the tree,
%% than might appear at the top-level (as the root node).
%% In order to specify a function which can traverse all nodes, not just the top-level nodes,
%% we have to include all possible node kinds in the type definition.
%% @doc `reduce_type/3' enables reducing an abstract type to a single value.
%%
%% Example 1 - gather all singleton atoms occurring in a type:
%%
%% ```
%% > F = fun
%% > ({atom, _, _} = At, Acc) -> {At, [At | Acc]};
%% > (Ty, Acc) -> {Ty, Acc}
%% > end,
%% > {_, [{atom, _, my_atom}]} = typelib:reduce_type(F, [], typelib:parse_type("A :: {my_atom}")).
%% '''
%%
%% `Fun' can skip traversing parts of the type tree by matching on it
%% and returning `none()' instead of the actual subtree.
%%
%% Example 2 - gather singleton atoms, but skip a particular branch of a union type:
%%
%% ```
%% > ComplexTy = typelib:parse_type("atom1 | atom2 | "
%% > "{complex, integer(), [{atom() | string(), number()}]}"),
%% > F = fun
%% > ({type, _, tuple, [{atom, _, complex} | _]}, Acc) -> { {type, 0, none, []}, Acc };
%% > ({atom, _, Name} = Ty, Acc) -> {Ty, [Name | Acc]};
%% > (Ty, Acc) -> {Ty, Acc}
%% > end,
%% > {_, [atom2, atom1]} = reduce(F, [], ComplexTy).
%% '''
%% @end
%% TODO: this is mostly redundant with `gradualizer_lib:fold_ast'
-spec reduce_type(Fun, Acc, walkable_type()) -> R when
Fun :: fun((walkable_type(), Acc) -> {walkable_type(), Acc}),
R :: {walkable_type(), Acc}.
reduce_type(Fun, Acc, Type) ->
reduce(Fun, apply, Acc, Type).
-spec reduce(Fun, Action, Acc, walkable_type()) -> R when
Fun :: fun((walkable_type(), Acc) -> {walkable_type(), Acc}),
Action :: apply | recurse,
R :: {walkable_type(), Acc}.
reduce(Fun, _, Acc, {'atom', _, _} = Ty) -> Fun(Ty, Acc);
reduce(Fun, _, Acc, {'type', _Anno, _Name, any} = Ty) -> Fun(Ty, Acc);
reduce(Fun, _, Acc, {'integer', _, _} = Ty) -> Fun(Ty, Acc);
reduce(Fun, _, Acc, {'char', _, _} = Ty) -> Fun(Ty, Acc);
reduce(Fun, _, Acc, {'type', _Anno, any} = Ty) -> Fun(Ty, Acc);
reduce(Fun, _, Acc, pos_inf = Ty) -> Fun(Ty, Acc);
reduce(Fun, _, Acc, neg_inf = Ty) -> Fun(Ty, Acc);
reduce(Fun, _, Acc, {var, _, _} = Ty) -> Fun(Ty, Acc);
reduce(Fun, apply, Acc, Ty) ->
{NewTy, Acc1} = Fun(Ty, Acc),
reduce(Fun, recurse, Acc1, NewTy);
reduce(Fun, recurse, Acc, {'op', _, _, Ty1}) -> reduce_rec(Fun, Acc, [Ty1]);
reduce(Fun, recurse, Acc, {'op', _, _, Ty1, Ty2}) -> reduce_rec(Fun, Acc, [Ty1, Ty2]);
reduce(Fun, recurse, Acc, {'ann_type', _Anno, Args}) -> reduce_rec(Fun, Acc, Args);
reduce(Fun, recurse, Acc, {'type', _Anno, _Name, Args}) -> reduce_rec(Fun, Acc, Args);
reduce(Fun, recurse, Acc, {'remote_type', _Anno, [M, T, As]}) -> reduce_rec(Fun, Acc, [M, T | As]);
reduce(Fun, recurse, Acc, {'user_type', _Anno, _Name, Args}) -> reduce_rec(Fun, Acc, Args).
reduce_rec(Fun, Acc, Args) ->
lists:foldl(fun (Arg, {_, Acc1}) ->
reduce(Fun, apply, Acc1, Arg)
end, {ok, Acc}, Args).