-
Notifications
You must be signed in to change notification settings - Fork 35
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
Conversation
-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()'. |
There was a problem hiding this comment.
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()?
There was a problem hiding this comment.
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
Gradualizer/src/gradualizer_type.erl
Lines 276 to 277 in 3cda2e5
-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
Gradualizer/src/gradualizer_type.erl
Line 298 in 3cda2e5
-type af_integer() :: {'integer', anno(), non_neg_integer()}. |
There was a problem hiding this 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()]), |
There was a problem hiding this comment.
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().
There was a problem hiding this comment.
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
Gradualizer/src/gradualizer_type.erl
Lines 290 to 292 in 3cda2e5
-type af_atom() :: af_lit_atom(atom()). | |
-type af_lit_atom(A) :: {'atom', anno(), A}. |
There was a problem hiding this comment.
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()
.
Gradualizer/src/gradualizer_type.erl
Lines 202 to 203 in 3cda2e5
-type abstract_type() :: af_annotated_type() | |
| af_atom() |
Doesn't this imply that {atom, anno(), atom()} :: abstract_type()
?
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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:
Gradualizer/src/gradualizer_type.erl
Lines 254 to 257 in 3cda2e5
-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()]
.
There was a problem hiding this comment.
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
.
src/typechecker.erl
Outdated
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🍰🎂🥧🍺
src/typechecker.erl
Outdated
@@ -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}; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 🤔
There was a problem hiding this comment.
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.
src/typechecker.erl
Outdated
NonEmptyTy = rewrite_list_to_nonempty_list(ListTy), | ||
{NonEmptyTy, NonEmptyTy, VEnv3, constraints:combine([Cs1, Cs2, Cs3])}; | ||
{PatTy, NonEmptyTy, VEnv3, constraints:combine([Cs1, Cs2, Cs3])}; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
@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. |
932f2f1
to
a57bf6c
Compare
For clauses that exhaust a list using patterns 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.) |
Assume we add 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.) |
a57bf6c
to
abec87f
Compare
abec87f
to
1a30b38
Compare
The current status is:
on master versus
on this branch. Less of a difference, but still worth it. |
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):
With this PR: