-
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
"The clause ... cannot be reached" when matching on list patterns #405
Comments
Apparently, this false negative is quite common in real world code. It pops up in Gradualizer results on two out of three OTP stdlib files which https://github.com/vrnithinkumar/ETC was benchmarked on: |
This is quite a common pattern in general. Examples in Gradualizer itself:
|
It's possible to tell from the pattern if it matches on a fixed-size list or on a list of any length - this could be used to make the refinement more precise: 3> merl:quote("fun ([a]) -> ok end").
{'fun',1,
{clauses,[{clause,1,
[{cons,1,{atom,1,a},{nil,1}}],
[],
[{atom,1,ok}]}]}}
4> merl:quote("fun ([a | L]) -> ok end").
{'fun',1,
{clauses,[{clause,1,
[{cons,1,{atom,1,a},{var,1,'L'}}],
[],
[{atom,1,ok}]}]}}
5> merl:quote("fun ([a | R]) -> ok end").
{'fun',1,
{clauses,[{clause,1,
[{cons,1,{atom,1,a},{var,1,'R'}}],
[],
[{atom,1,ok}]}]}}
6> merl:quote("fun ([a | _]) -> ok end").
{'fun',1,
{clauses,[{clause,1,
[{cons,1,{atom,1,a},{var,1,'_'}}],
[],
[{atom,1,ok}]}]}}
7> merl:quote("fun ([a, b | _]) -> ok end").
{'fun',1,
{clauses,[{clause,1,
[{cons,1,{atom,1,a},{cons,1,{atom,1,b},{var,1,'_'}}}],
[],
[{atom,1,ok}]}]}}
8> merl:quote("fun ([a, b]) -> ok end").
{'fun',1,
{clauses,[{clause,1,
[{cons,1,{atom,1,a},{cons,1,{atom,1,b},{nil,1}}}],
[],
[{atom,1,ok}]}]}} Let's write
We'd still have a problem with -spec i([atom()]) -> ok.
i([]) -> ok;
i([Cs]) -> ok;
i([C1, C2 | Cs]) -> ok. Would be refined as follows: list(atom) -- we start with the spec
list(atom()) \ nil() -> nonempty_list(atom()) -- we refine with the first clause pattern []
nonempty_list(atom()) \ list(atom(), 1) -> list(atom()) -- we refine with the fixed-length pattern [Cs] from the second clause
list(atom()) \ list(atom()) -> none() -- we refine with the arbitrary (but at least 2 element long) length pattern [C1, C2 | Cs] from the third clause
none() -- all clauses are reachable and exhaust the input type list(atom) Now back to why |
@zuiderkwast @berbiche WDYT? |
I think we have discussed this long ago: extending the type language with fixed-length lists. I think we should avoid extending the type language seen by the user. We do need to distinguish between list patterns that exhausts a list type and one that doesn't though, internally. A pattern like |
👍
I'm not sure yet, but I think we can. We'll see if that pans out, but I hope so! |
Let's try another refinement example: -spec j([atom()]) -> ok.
j([]) -> ok;
j([C1, C2 | _]) -> ok;
j([Cs]) -> ok. The refinement would go as follows: list(atom) -- we start with the spec
list(atom()) \ nil() -> nonempty_list(atom()) -- 1st clause
nonempty_list(atom()) \ nonempty_list(atom()) -> none() -- 2nd clause
none() \ list(atom(), 1) -> ? This manifests in practice with (using erszcz@0b3bec6):
What if we could store the length of the list type inferred from a list pattern? list(atom(), 0+) -- we start with the spec
list(atom(), 0+) \ list(top(), 0) -> list(atom(), 1+) -- 1st clause
list(atom(), 1+) \ list(atom(), 2+) -> list(atom(), 1) -- 2nd clause
list(atom(), 1) \ list(atom(), 1) -> none() -- 3rd clause
none() -- hooray \o/ |
Yes, if we encode length in the list type, we could do exhaustiveness checking in this case. If we don't do it, we can still create a sound type system and just not do exhaustiveness-checking in this case, if we consider exhaustiveness-checking a bonus, i.e. not a guarantee for all lists of clauses. The error is that we perform the 2nd refinement. We shouldn't, because The example again: -spec j([atom()]) -> ok.
j([]) -> ok;
j([C1, C2 | _]) -> ok;
j([Cs]) -> ok. This is what we should do (if we don't introduce
|
Hmm, indeed, it might make sense to disable it for lists 🤔 Exhaustiveness checking makes the most sense for variant types anyway. |
I think we can do exhaustiveness-checking for lists in some cases, but not always. For example, we can do it for In general, I think we can disable exhaustiveness-checking whenever |
Even with #404 when running
make gradualize
we get this report:The problematic code from
constraints.erl
when minimised gives the following example:The thrown error is:
Which means that if the argument type is
none
when checking a clause, it's considered as unreachable. Now, let's see how the argument type is refined as we check subsequent clauses:type(list, [atom()])
from the function spec.type(nonempty_list, [atom()])
. The pattern of the second clause is inferred astype(nonempty_list, [atom()])
. This leads to the following refinement after the second clause (1st param is the type being refined, 2nd param is the second clause pattern type):This means that the input type to the third clause is
type(none)
. Without using extra information about the list pattern length we won't be able to disambiguate between types of the second and third clauses of the function.I'm not sure what the right thing to do is:
unreachable_clause
at all? That would be a significant loss of useful functionality (this warning is one of the core benefits shown in Gradient's TypedServer demo) :(The text was updated successfully, but these errors were encountered: