-
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
Experimental constraint solver #450
Conversation
Actually, I don't think |
849b4ad
to
fcc3833
Compare
I've rebased this on top of the current master and force pushed. Solving constraints is currently enabled by a command-line flag or an option set via an attribute. It's enabled for the self-check, which bumps the number of errors above 100 lines - that's why CI fails. It's not a lot more - 116 lines - but still more than the self-imposed threshold of 100. I'm also gathering examples that would be useful as tests (currently the
|
9e03120
to
84490d4
Compare
I think this is ready for review now. My rationale for merging this in is that on a feature branch it's subject to bit rot, whereas on master it will be regularly tested for compatibility with the rest of the project thanks to CI. Main changes from #284:
Constraint solving will certainly require more work to be fully functional, especially in the light of #488, but I think it's better to have whatever already works merged to master. @josefs @zuiderkwast WDYT? |
I've fixed a few more self-check errors. Do you have any comments on this, @josefs @zuiderkwast? Especially any comments vetoing a merge into master? I'd like to rebase this onto master and merge after #499. |
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.
The tests look good. It's nice to see that the solver does something useful.
I've only skimmed thought the implementation. I think it's a good idea to merge this with constraint solving off by default, as you suggested.
Is the solver implemented as if subtyping is transitive? This is what Josef mentioned, but I haven't really spotted this part of the implementation. It would be nice to add some example of what the solver can't do if it assumes typing is transitive, which it actually isn't when any() is involved. (X :: Y and Y :: Z doesn't imply X :: Z if Y is any()).
-spec poly_2(fun((A) -> A)) -> {integer(), boolean()}. | ||
poly_2(F) -> {F(42), F(false)}. |
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.
This one shouldn't fail. Should it?
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 don't feel completely comfortable with this topic yet, but I think it depends on the future goals: if we ever want to be able to infer the type of a function like poly_2
(for example, when it's used as an inline fun), then we're limited to rank-2 polymorphism for which inference is decidable.
According to Bidirectional Typing for Erlang:
Erlang allows higher-ranked polymorphism where polymorphic types occur anywhere in the type signature. The Hindley-Milner type inference only supports the inference of rank-1 (prenex) polymorphism. Type inference for the higher-ranked polymorphism is undecidable, which is a significant limitation for the Hindley-Milner type inference. For example, the
poly_2
function shown next has rank-2 polymorphism that ETC fails to type-check:-spec poly_2(fun((A) -> A)) -> {integer(), boolean()}. poly_2(F) -> {F(42), F(false)}.
Similarly, ETC fails for
poly_fail/3
due to its rank-2 polymorphism.-spec poly_fail(fun((A) -> A), boolean(), integer()) -> {boolean(), integer()}. poly_fail(F, B, I) -> {F(I), F(B)}.
In contrast, Dialyzer here finds the return type mismatch error.
As we can see from this test, Gradualizer is also able to detect this error.
I'd leave it as is until anyone of us is comfortable enough with constraint solving and polymorphism to confidently move it to should_pass
tests.
This is my first stab at the constraint solver. It is only very lightly tested and is not hooked in to the type checker yet.
When checking toplevel functions, call the constraint solver to work out if the existential types are correct. Unimplemented: * Applying the substitution on the remaining types. * Check that the remaining constraints are compatible with the constraints declared in the spec. I fixed a few bugs in the process.
A variable might not have any constraints and that's fine!
Keeping the constraints when checking unions requires having a union of constraints and the current implementation doesn't support that. So right now we just drop the constraints. This means that we currently don't report as many type errors as one might expect.
The current representation for constraints cannot handle union types so the constraints are dropped for now. This means that some type errors will not be caught.
The function instantiate_fun_type now returns constraints which have to be propagated. This was caught by dialyzer
…d_should_fail.erl to test/should_fail/poly_union_lower_bound_fail.erl
…to test/should_fail/poly_fail.erl
…st/known_problems/should_pass/poly_lists_map_constraints_should_pass.erl
…ould_pass.erl -> test/should_pass/poly_lists_map_constraints_pass.erl
Based on: -spec get_unassigned_fields(Fields, All) -> [atom()] when Fields :: [record_field() | typed_record_field()], All :: [typed_record_field()]. get_unassigned_fields(Fields, All) -> FieldNames = lists:flatmap(fun (?record_field(Field)) -> [Field]; (?typed_record_field(Field)) -> [Field]; %% We might run into values like this, which don't match above: %% {record_field, _, {var,{12,11},'_'}, _} (_) -> [] end, Fields), AllNames = lists:map(fun (?typed_record_field(Field)) -> Field end, All), AllNames -- FieldNames. Which currently raises: ebin/typechecker.beam: Lower bound record_field() | typed_record_field() of type variable A_typechecker_3467_838 on line 2026 is not a subtype of {A_typechecker_3467_838_typechecker_1226_848, A_typechecker_3467_838_typechecker_1226_849, A_typechecker_3467_838_typechecker_1226_850}
…o test/should_pass/poly_union_lower_bound_pass.erl
…empty constraints
Based on: -spec combine_clauses_argument_constraints([type()], constraints:t(), map(), env()) -> constraints:t(). combine_clauses_argument_constraints(ArgsTys, Cs, CatchAllArgs, Env) -> ArgsTyVars = lists:flatmap(fun ({var, _, Var}) -> [Var]; (_) -> [] end, ArgsTys), ... The catch-all pattern does not register a constraint, while the tuple pattern does. Because of that the type checker fails matching any type() with the funs clauses and returns a warning, even though any type() instance will be handled by the fun.
758850e
to
1f78c53
Compare
I've merged #499 to master and rebased this PR on top of that. The self-check has uncovered a number of issues with functions with multiple clauses registering too narrow constraints on type variables. I came up with the idea that an argument type var should be constrained with a union of types accepted by all of such a fun's clauses. Let's consider -spec flatmap(Fun, List1) -> List2
when
Fun :: fun((A) -> [B]),
List1 :: [A],
List2 :: [B]. -type t1() :: {_, _, _, _}.
-type t2() :: {_, _, _}.
-spec f([t1() | t2()]) -> ok.
f(Fields) ->
lists:flatmap(fun
({_, _, _, _}) -> [t1];
({_, _, _}) -> [t2]
end, Fields),
ok. When constraining A we must take all of the inline fun's clauses into account, i.e. constrain Another aspect of that can be seen in the following example: -spec foldl(Fun, Acc, List) -> Acc
when
Fun :: fun((T, Acc) -> Acc),
List :: [T]. -spec g([t1() | t2()]) -> ok.
g(Fields) ->
lists:foldl(fun
({_, _, _, _}, Acc) -> [t1 | Acc];
(_, Acc) -> [default | Acc]
end, [], Fields),
ok. The I was afraid the idea of a union upper bound constraint might be a bit fragile, but so far it seems to work quite well 👍 |
This version brings approx. 30 new PRs. The highlights are: - Improve map exhaustiveness checking [erlang-ls#524](josefs/Gradualizer#524) by @xxdavid - Fix all remaining self-check errors [erlang-ls#521](josefs/Gradualizer#521) by @erszcz - Fix intersection-typed function calls with union-typed arguments [erlang-ls#514](josefs/Gradualizer#514) by @erszcz - Experimental constraint solver [erlang-ls#450](josefs/Gradualizer#450) by @erszcz
This version brings approx. 30 PRs. The highlights are: - Improve map exhaustiveness checking [erlang-ls#524](josefs/Gradualizer#524) by @xxdavid - Fix all remaining self-check errors [erlang-ls#521](josefs/Gradualizer#521) by @erszcz - Fix intersection-typed function calls with union-typed arguments [erlang-ls#514](josefs/Gradualizer#514) by @erszcz - Experimental constraint solver [erlang-ls#450](josefs/Gradualizer#450) by @erszcz
This version brings approx. 30 PRs. The highlights are: - Improve map exhaustiveness checking [erlang-ls#524](josefs/Gradualizer#524) by @xxdavid - Fix all remaining self-check errors [erlang-ls#521](josefs/Gradualizer#521) by @erszcz - Fix intersection-typed function calls with union-typed arguments [erlang-ls#514](josefs/Gradualizer#514) by @erszcz - Experimental constraint solver [erlang-ls#450](josefs/Gradualizer#450) by @erszcz
This version brings approx. 30 PRs. The highlights are: - Improve map exhaustiveness checking [#524](josefs/Gradualizer#524) by @xxdavid - Fix all remaining self-check errors [#521](josefs/Gradualizer#521) by @erszcz - Fix intersection-typed function calls with union-typed arguments [#514](josefs/Gradualizer#514) by @erszcz - Experimental constraint solver [#450](josefs/Gradualizer#450) by @erszcz
This rebases #284 by @josefs onto the current master branch. It also adds some polymorphic function tests borrowed from Bidirectional typing for Erlang to show the solver works - at least in some cases.
Constraint solving is enabled by using a Gradualizer option
solve_constraints
. Without the option the behaviour is unchanged. I think this approach will allow to merge this PR early, yet make the functionality opt-in while it's being polished.I'm making this PR a draft for now, since it shows some suspicious reports when running a self-check:
In general, the number of warnings on a self-check has grown significantly with this change. However, that was to be expected.