Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix some self-gradualization errors #408

Merged
merged 6 commits into from
May 30, 2022

Conversation

erszcz
Copy link
Collaborator

@erszcz erszcz commented Apr 3, 2022

This builds on top of #404, which cleans up a considerable number of regressions from make gradualize output, and improves on it by fixing spec errors and other type issues in Gradualizer itself.

It's also a good exercise to find bugs in Gradualizer or spot hard to type check Erlang idioms.

Before this PR (but already with #404):

$ make gradualize | wc -l
make: *** [gradualize] Error 1
     619

With this PR:

$ make gradualize | wc -l
make: *** [gradualize] Error 1
     352

-type extended_int_type() :: {type, erl_anno:anno(), range, [{integer, erl_anno:anno(), int()}]}
| {'integer', erl_anno:anno(), integer()}.
%% `extended_int_type' is needed to describe type representations
%% which are not part of `gradualizer_type:abstract_type()'.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this not a subtype to type()?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In ranges, if we use pos_inf or neg_inf, it's not wrapped in a {integer, Anno, _} tuple, see

-type af_range_integer_type() :: 'pos_inf' | 'neg_inf'
| af_singleton_integer_type().

With integers, the number is always a non_neg_integer(), if it has to be a negative value, it's wrapped in a unary minus op node, see

-type af_integer() :: {'integer', anno(), non_neg_integer()}.

Copy link
Collaborator

@zuiderkwast zuiderkwast left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice!

get_type_definition({remote_type, _Anno, [{atom, _, Module}, {atom, _, Name}, Args]}, _Env, _Opts) ->
%% We matched out the non-type() elements above so we can assert Args :: [type()]
Args = ?assert_type(Args, [type()]),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? Atom literals are part of abstract_type().

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Atom literals can be represented by type(), but literal atoms are not of type type(), see

-type af_atom() :: af_lit_atom(atom()).
-type af_lit_atom(A) :: {'atom', anno(), A}.

Copy link
Collaborator

@zuiderkwast zuiderkwast Apr 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

abstract_type() includes af_atom().

-type abstract_type() :: af_annotated_type()
| af_atom()

Doesn't this imply that {atom, anno(), atom()} :: abstract_type()?

Copy link
Collaborator Author

@erszcz erszcz Apr 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, sorry, my above comment is nonsense, as the atoms here are actually in the AST representation. Let me check again...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, so we start with this error:

$ gradualizer -I include/ -- src/gradualizer_lib.erl
src/gradualizer_lib.erl: The variable on line 112 at column 43 is expected to have type [type()]
but it has type [abstract_type()] | {atom, anno(), atom()}

    %% We matched out the non-type() elements above so we can assert Args :: [type()]
    %Args = ?assert_type(Args, [type()]),
    gradualizer_db:get_type(Module, Name, Args);
                                          ^^^^

We check the type of a remote call node:

-type af_remote_type() ::
{'remote_type', anno(), [(Module :: af_atom()) |
(TypeName :: af_atom()) |
[abstract_type()]]}. % [Module, Name, [T]]

So Args is a [type()], not a type(), which is fine for passing down. Singleton atoms are not fine, as we need a list. We know we've already matched on the singleton atoms, so by elimination we're safe to assert Args :: [type()].

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! Only the comment needs an update then. E.g. %% We matched out the single atom arguments, so only Args (list-of-type) remains.

refine_ty(?type(list, E), ?type(nonempty_list, E), _, _Env) ->
type(nil);
refine_ty(?type(list, [ElemTy1]), ?type(nonempty_list, [ElemTy2]), Trace, Env) ->
case refine(ElemTy1, ElemTy2, Trace, Env) of
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🍰🎂🥧🍺

@@ -4227,14 +4250,15 @@ add_type_pat(CONS = {cons, P, PH, PT}, ListTy, Env, VEnv) ->
TailTy = normalize(type(union, [ListTy, type(nil)]), Env),
{_TailPatTy, _TauUBound, VEnv3, Cs} = add_type_pat(PT, TailTy, Env, VEnv2),
NonEmptyTy = rewrite_list_to_nonempty_list(ListTy),
{NonEmptyTy, NonEmptyTy, VEnv3, Cs};
{type(none), NonEmptyTy, VEnv3, Cs};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[_|_] exhausts nonempty list. Have you saved that for another PR?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, I have to think it through 🤔

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's only the pattern [A | B] where A and B are free variables (not in VEnv) that works.

NonEmptyTy = rewrite_list_to_nonempty_list(ListTy),
{NonEmptyTy, NonEmptyTy, VEnv3, constraints:combine([Cs1, Cs2, Cs3])};
{PatTy, NonEmptyTy, VEnv3, constraints:combine([Cs1, Cs2, Cs3])};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, I think we should return {none(), ...} too, in the normal case.

Only if the pattern is match-all ([A|B] where A and B are free, or A is bound and ElemTy is a singleton type) the pattern exhausts the type. In that case, we can return PatTy I suppose.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the comment above add_types_pats about the return tuple {PatTys, UBounds, NewVEnv, Constraints}:

%% The returned lists of types are interpreted like this:
%% PatTy :: Pat as if Pat were a type. For match-all patterns, PatTy
%% is the same as the type. For patterns matching a singleton type, PatTy
%% is the singleton type. Otherwise, PatTy is none(). PatTy is a type exhausted
%% by Pat. UBound is Ty or a subtype such that Pat :: UBound.

@erszcz
Copy link
Collaborator Author

erszcz commented Apr 5, 2022

@zuiderkwast I've given your points about exhaustive list types some thought. I also considered if just looking at whether the patterns match fixed length lists or any lists is sufficient - I'm afraid it's not :/ It seems that to model cases like these:

-spec i([atom()]) -> ok.
i([]) -> ok;
i([Cs]) -> ok;
i([C1, C2 | Cs]) -> ok.

-spec j([atom()]) -> ok.
j([]) -> ok;
j([C1, C2 | _]) -> ok;
j([Cs]) -> ok.

it's necessary to track the lengths of the patterns. Please see #405 (comment) for a bit more detail.

@zuiderkwast
Copy link
Collaborator

it's necessary to track the lengths of the patterns

For clauses that exhaust a list using patterns [], [X] and [X, Y | Z], we can't perform exhaustiveness-checking without encoding the list length in the type, but that doesn't mean we can't silence the false positives and make it pass.

I believe we can run into similar problems for other types and there will always be something we can't do, unless we continue to extend the type language over and over. I'm not saying we can't do that, but I'm almost certain that we'll run into a lot of times when we need to decide how subtyping, GLB, refinement, etc. work for these types. We need draw the line somewhere. (We already have some overly fancy logic for arithmetic operators, for example, that I think we could live without, to make the type system simpler, but I know others think it's worth it.)

@zuiderkwast
Copy link
Collaborator

zuiderkwast commented Apr 5, 2022

Assume we add list(T, N) and list(T, N+) types.

Should we be able do exhaustiveness-checking for the following?

-spec q([fruit()]) -> fruit().
q([X | Xs]) when length(Xs) rem 2 == 0 ->
    X;
q(Xs) when length(Xs) rem 2 == 0 ->
    bananas.

(We could solve it if we encode odd and even lengths of lists in the type. Not saying we should.)

@erszcz erszcz force-pushed the fix-self-gradualization-errors branch from a57bf6c to abec87f Compare April 7, 2022 22:03
@erszcz erszcz force-pushed the fix-self-gradualization-errors branch from abec87f to 1a30b38 Compare May 30, 2022 08:01
@erszcz
Copy link
Collaborator Author

erszcz commented May 30, 2022

The current status is:

10:02:04 erszcz @ x6 : ~/work/erszcz/gradualizer ((a72e7f0...) %)
$ make gradualize | wc -l
make: *** [gradualize] Error 1
     485

on master versus

10:02:24 erszcz @ x6 : ~/work/erszcz/gradualizer (fix-self-gradualization-errors %)
$ make gradualize | wc -l
make: *** [gradualize] Error 1
     361

on this branch. Less of a difference, but still worth it.

@erszcz erszcz merged commit 6ae3298 into josefs:master May 30, 2022
@erszcz erszcz deleted the fix-self-gradualization-errors branch May 30, 2022 08:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants