-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Implement exhaustiveness check #4837
Implement exhaustiveness check #4837
Conversation
You can find other cases in spec. |
|
||
tuple_patterns.to_a | ||
.sort_by do |tuple_pattern| | ||
tuple_pattern.map do |pattern| |
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 indentation looks formatter's bug, however it is another problem for this.
53bf6bd
to
7372c05
Compare
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 the feature is great. I also think I'll hate that feature.
@ysbaddaden I understand what you said. I want you to try to use this compiler to your project. |
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.
Leads to more verbose code but probably worth it
last_last = last_last_else | ||
end | ||
if last_last.else.is_a?(Nop) | ||
last_last.else = Call.new(nil, "raise", args: [StringLiteral.new("BUG: invalid exhaustivness check")] of ASTNode, global: true).at(node) |
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.
exhaustivness -> exhaustiveness
|
||
case case_cond_type | ||
when UnionType | ||
# 'dup' is important to prevent to change this array value by mutable methods. |
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.
to change -> changing
@@ -117,12 +117,12 @@ class HTTP::Client | |||
{% else %} | |||
def initialize(@host : String, port = nil, tls : Bool | OpenSSL::SSL::Context::Client = false) | |||
@tls = case tls | |||
when true | |||
OpenSSL::SSL::Context::Client.new | |||
when Bool |
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.
So case now checks either types or enums, not both?
Nvm, i've checked and it works with old code too. So you just changed it for clarity?
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.
No, it's just my mistake. Firstly I implemented exhaustiveness check without supporting true
and false
. It is remains of this.
|
@asterite I'll fix it. |
What I like in this PR is making the compiler smarter, so it knows that a What I don't like in this PR is trading implicit nilables and "I don't care for other values" for explicit noise with empty I verified most of my applications and libraries, and this PR will help in... none. Quite the opposite, as it will require me to insert many empty The benefit, which is inexistant in all my projects (and it seems the compiler source too), isn't worth the burden. This is an unacceptable trade IMHO. |
@asterite I can't help but think that without global type inference what would be the point of crystal at all. Global type inference with unions is the defining feature of Crystal. To me - at least - it's what makes Crystal, Crystal. I think it'd be a huge mistake to change Crystal's type system in such a radical way this late in development, and it'd be a huge mistake to throw away something which is so beautiful and easy to use. Yes the compile times for large program suck, perhaps we'll have to introduce some "module" concept with defined types for the interface, but languages like Scala managed to succeed despite long compile times. Perhaps people using crystal will create more microservices than other languages. I'd rather have a discussion like this on the mailinglist though. |
Maybe we can use some special keyword to check exhaustiveness This code would compile ok: enum FooBar
Foo
Bar
end
foo = FooBar::Foo
case foo
when .foo?
end But this won't: enum FooBar
Foo
Bar
end
foo = FooBar::Foo
@[Exhaustive]
case foo
when .foo?
end See the
|
I want the compiler to check exhaustiveness in default because exhaustiveness is important to prevent bugs, so I want you to consider about it at all times. However many people need this, I suggest |
If we are going to be exhaustive by default then |
I think exhaustiveness check to existed project affects us not helpful because all non-exhaustive |
I actually prefer being exhaustive by default. It gives me peace of mind and most importantly a confidence to refactor and add {things to enums,types to unions} without worrying about every |
We may need the attribute to drop exhaustiveness check from @[NotCheckExhaustiveness]
enum KeyCode
A
B
# ...
end
key = KeyCode::A
# This `case` passes exhaustiveness check due to @[NotCheckExhaustiveness] attribute.
# Of course result type is nilable.
case key
when .w?
go
when .a?
turn_left
when .s?
back
when .d?
turn_right
end /cc @ysbaddaden |
@makenowjust The problem is that that makes case behaviour dependant on an attribute on enum which is unlikely to be visible to the programmer at the point the enum is used. |
I found a failed exhaustiveness check in callstack.cr:205. |
@straight-shoota I think it's already fixed. Your stdlib may be old. |
@RX14 Exhaustiveness check to |
Now it is working: a = 42
loop do
case a
when Int32
end
a = "foo"
end
# Error in foo.cr:3: instantiating 'loop()'
#
# loop do
# ^~~~
#
# in foo.cr:3: instantiating 'loop()'
#
# loop do
# ^~~~
#
# in foo.cr:4: found non-exhaustive pattern: String
#
# case a
# ^ /cc @asterite |
|
||
if exhaustive? != is_exhaustive | ||
last = expansion | ||
last = last.expressions.last if last.is_a?(Expressions) |
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.
MEMO: last.expressions.last
can be replaced to last.last
.
256c621
to
abb4f32
Compare
ping. I don't want to stop discussion about this. |
I personally think having |
Ref crystal-lang#4837 I think this syntax has no use-case. It can be replaced with 'else' block always.
Actually, about hierarchy type, maybe it's good not to check for exhaustiveness because code will break if a subclass is added in a separate library or application, and there's no way to fix that (you don't own the code that has the case). So probably with just unions and enums it's fine... Though with unions you could also have the same problem if someone redefines a method, but that's less common. |
@makenowjust どうもありがとうございます! |
Ship it! Having this in 0.25.0 would be really nice, both because it improves the robustness of code, and because it can be tested early in the wild. |
Ref #4837 I think this syntax has no use-case. It can be replaced with 'else' block always.
Letting the So I think the exhaustive vs non-exhaustive check should be explicit. Having different keywords it is a clear way to do it: But I think there is a different approach that could be more powerful. case expr
when Int32
when String
else unreachable
end Then all the It is longer to type that a different control structure keyword but is a feature that can be combined with other control structures eventually if redundant code is detected. Note: The implementation would probably not be that different from this PR. The compile time error needs to be deferred, but the exhaustive check is the hardest part. For me is either unreachable or a different case statement (I prefer unreachable between those). Having to add |
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.
☝️
So currently, this PR falls back to non-exhaustive case depending on the type of I don't like Perhaps use |
As for how to implement |
No. It checks exhaustiveness for any typed |
If the case |
@bcardiff Sorry. I forgot this specification. Oh I see. It is bad point of this. However it needs more and more (180+ cases) appending
|
It's a subtle thing and is hard to be everybody in the same page. Maybe the Just to be clear, I was previously proposing the following constructs to allow exhaustive checks: forall a
when String
end over a
when String
end @RX14 suggestions to leave |
Ref crystal-lang#4837 I think this syntax has no use-case. It can be replaced with 'else' block always.
@asterite I think your opinion is needed to proceed with discussion. bcardiff opinion is right and his ideas are interesting to me. What do you think? |
@bcardiff said:
But that's not true.
I personally don't mind having to add an So for me, this PR is perfect. And I would have liked it to be in 0.25.0. But ultimately, I don't use Crystal anymore, so I shouldn't choose its future. |
Agreed. As long as it’s exhaustive in all cases I think this should work great. I can hardly wait to try this :) |
Yes, let's keep it simple, please. I don't like having different keywords with subtle differences. People will be confused. |
@asterite no, this PR isn't exhaustive in all cases, for example: str = "bar"
case str
when "foo"
1
end will not produce a compile error and be type This is what we meant by the exhaustiveness depending on the argument. Let's try making |
Oh, I thought that was already the case, because I see else over symbols in the diff, and other primitives. So yes, either change it to always be exhaustive, or only exhaustive when it makes sense (I'm fine with that too, and I find it intuitive and user friendly, as forcing al else clause over something that the compiler can't verify is a bit useless) |
I'd really love to see this PR merged. More correctness is great. |
Note that this PR is incomplete and doesn't play nicely with the type flow inference. For example: a = 1 || "foo"
case a
when Int32
b = 1
when String
b = 2
end
puts typeof(b) # => Int32 | Nil That is, the compiler correctly finds that it's an exhaustive match, but the type for (that's at least the last thing I remember when I tried this PR, now it needs a rebase but I'm pretty sure it'll still have that problem) |
I think we really need to form a consensus on the design first before fixing the implmentation because there are some unresolved issues. I think the current prevailing design is for The benefits of being exhaustive in this case are:
The benefits of not being exhaustive in this case are:
There's also the option to make it so that case is exhaustive if just one of the types in the union can be exhaustively matched. This might make a better semantic in practice, but retains a lot of complexity. Ideally we'll have a continued discussion of the pros and cons of these three choices of semantics below then a vote in a week or so. Merging any of those three semantics will be better than what we have currently. |
This can probably be closed now that #8424 is merged? |
Yes, but there is a lot of credit to go on this PR and the discussion 🙇. |
Implemented exhaustiveness check like Rust or Scala. It works for only union types and enums (and
nil
,true
andfalse
). For example: