Skip to content

Extend exhaustivity checks to and-types #2155

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

Merged
merged 6 commits into from
Apr 26, 2017
Merged

Extend exhaustivity checks to and-types #2155

merged 6 commits into from
Apr 26, 2017

Conversation

abgruszecki
Copy link
Contributor

As discussed on gitter, this PR extends exhaustivity checks to and-types. It's possible to check exhaustivity of pattern matching on them in case of "marker" types like this:

trait Marker
sealed type T
case object A extends T with Marker
case object B extends T with Marker
case object C extends T
def m(s: T & Marker) = s match { case A => ; }
// current behaviour without -YcheckAllPatmat:
//     no warning, since and-types aren't considered checkable
// with -YcheckAllPatmat:
//     warns that _: T & Marker would fail - T & Marker isn't decomposed into A | B

I've defined and-types as checkable if either type is checkable, decomposable if either type is decomposable and used space intersection for decomposition. However, this gives wrong results for this case:

sealed trait T
trait T1 extends T
trait T2 extends T
trait T3
def m(s: T & T3) = s match { case _: T1 => ; }

The underlying issue is that the intersection of two open types was previously approximated to be empty. I've added a check to intersect to see if it's possible to have a value of both types and return a type space of the and-type if that's the case. This changes slightly how space subtraction behaves - it's now possible to get "degenerate" unions from subtracting two constructor spaces ("degenerate" in the sense that one of the spaces is exactly the left-hand-side of the subtraction). This doesn't change the final result of exhaustivity check, though, so I'm not sure if it's an issue.

I'm also not completely sure if I got the isOpen implementation right - I'm not really familiar with how symbols work, so it's entirely possible there's a mistake there.

I'm also aware that commonOpenTypeSpace may sometimes return a space that can't be inhabited, since for instance the types are classes with conflicting methods, but I don't think it's an issue, since a) such and-types should be rare and b) we will at most emit a warning that the match may not be exhaustive.

/cc @liufengyun

@smarter
Copy link
Member

smarter commented Mar 30, 2017

http://dotty-ci.epfl.ch/lampepfl/dotty/1516/5 :

[error] Test dotty.tools.dotc.CompilationTests.runAll failed: java.lang.AssertionError: Run test failed, but should not, took 288.947 sec
[error] at dotty.tools.dotc.ParallelTesting$CompilationTest.checkRuns(ParallelTesting.scala:811)
[error] at dotty.tools.dotc.CompilationTests.runAll(CompilationTests.scala:166)
[error] ...

@felixmulder This is not a very precise error message :).

@liufengyun
Copy link
Contributor

Thanks for the quick turnaround @AleksanderBG . While waiting to see the failures, I think it's possible to merge isOpen and commonOpenTypeSpace into one method to simplify the protocol, as they are used exactly at the same place. Maybe something named intersectAtomTypes? The name atom is justified by the fact that an un-decomposable type must be matched as a whole.

intersectAtomTypes can be further optimised by single inheritance of Scala class system. Namely, if A and B both refer to classes types, then the intersection is empty.

@felixmulder
Copy link
Contributor

@smarter: re-ran it locally, seems to work just fine...amending the test suite tomorrow to dump all the logs on any failure. My best guess is that something threw an exception and the test called fail() without printing to stdout/stderr.

*/
def commonOpenTypeSpace(tp1: Type, tp2: Type): Space
/** Return a space containing the values of both atom (non-decomposable) types. */
def intersectAtomTypes(tp1: Type, tp2: Type): Space
Copy link
Member

Choose a reason for hiding this comment

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

nit-pick: I would say AtomicTypes instead of AtomTypes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can do - that does sound more correct than the other one.

@abgruszecki
Copy link
Contributor Author

abgruszecki commented Apr 2, 2017

I've merged isOpen/commonOpenTypeSpace as suggested, and checked if at least one of the types is a trait - I hope the code does the right thing. I've tried to read the documentation about symbols, but they're still confusing.

Regarding the test failures, I've been running test locally instead of partest - the second one fails with this message, even on master:

[error] Test dotc.tests.pickle_pickleOK failed: scala.reflect.io.FileOperationException: File 'innerclass.flags' already exists., took 0.019 sec
[error]     at scala.reflect.io.Path$.fail(Path.scala:78)
[error]     at scala.reflect.io.Path.createFile(Path.scala:235)
[error]     at dotty.tools.dotc.CompilerTest.copyFiles(CompilerTest.scala:468)
[error]     at dotty.tools.dotc.CompilerTest.computeDestAndCopyFiles(CompilerTest.scala:446)
[error]     at dotty.tools.dotc.CompilerTest.compileFile(CompilerTest.scala:116)
[error]     at dotty.tools.dotc.CompilerTest.$anonfun$compileFiles$2(CompilerTest.scala:210)
[error]     at scala.compat.java8.JProcedure1.apply(JProcedure1.java:18)
[error]     at scala.compat.java8.JProcedure1.apply(JProcedure1.java:10)
[error]     at scala.collection.IndexedSeqOptimized$class.foreach(IndexedSeqOptimized.scala:33)
[error]     at scala.collection.mutable.ArrayOps$ofRef.foreach(ArrayOps.scala:186)
[error]     at dotty.tools.dotc.CompilerTest.compileFiles(CompilerTest.scala:211)
[error]     at dotc.tests.pickle_pickleOK(tests.scala:110)
[error]     ...

Is there something I should be doing apart from just ;clean; test:compile; partest --show-diff --verbose?

@smarter
Copy link
Member

smarter commented Apr 2, 2017

@AleksanderBG partest is going to be retired soon so it might not be worth trying to get it working now, but one easy thing you can try is to delete tests/partest-generated. Regarding symbols, etc: if you have specific questions please don't hesitate to ask here or on gitter, otherwise you might be interested in watching this talk which explains some of the core data structures of dotty: https://www.youtube.com/watch?v=WxyyJyB_Ssc (slides: https://www.slideshare.net/Odersky/compilers-are-databases)

Copy link
Contributor

@liufengyun liufengyun left a comment

Choose a reason for hiding this comment

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

We are making some big changes to the CI recently, the CI is most likely to be some instability -- but the partest failure might indicate some problem with our new partest framework. \cc @felixmulder.


if ((tp1.classSymbol.is(Trait) || tp2.classSymbol.is(Trait))
&& isOpen(tp1) && isOpen(tp2))
Typ(AndType(tp1, tp2), true)
Copy link
Contributor

Choose a reason for hiding this comment

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

Normally, we expect programmers will not abuse AND-types in pattern matches. However, to be safer, it's better to handle the case if tp1 and tp2 are themselves AND-types if it doesn't significantly complicate the implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Could you elaborate how and-types could be abused here? Judging from the classSymbol implementation, if the type is an and-type, a symbol will be returned only if the and-type is "effectively" a class/trait, which seems to be correct in this case.

We could potentially simplify the and-type here if that's the case, though.

Copy link
Contributor

Choose a reason for hiding this comment

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

What I've in mind is the case where the two types are Trait1 & Trait2 and Trait3 & Trait4, in which case, the intersection would be Trait1 & Trait2 & Trait3 & Trait4.

@@ -84,6 +84,9 @@ trait SpaceLogic {
/** Is `tp1` the same type as `tp2`? */
def isEqualType(tp1: Type, tp2: Type): Boolean

/** Return a space containing the values of both atom (non-decomposable) types. */
def intersectAtomTypes(tp1: Type, tp2: Type): Space
Copy link
Contributor

Choose a reason for hiding this comment

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

Better to add one additional note saying that tp1 and tp2 are not subtype of each other.

@liufengyun
Copy link
Contributor

BTW, @AleksanderBG I'm not sure if the CLA fails because you haven't signed the CLA? If not, you can do it below:

https://www.lightbend.com/contribute/cla/scala

@abgruszecki
Copy link
Contributor Author

@smarter thank you very much for the presentation - it was quite helpful!
@liufengyun I've handled nested and-types, as well as type aliases and refined types. I'm not sure if the code meets the Dotty standards of efficiency, but I think it's at least correct. I didn't handle structural types, since isSubType treats them conservatively as a supertype of all other types - I don't think we can improve on that without checking for member collisions, since otherwise it'd be possible to miss a case, like here:

sealed trait T
object O { val x = 5 } extends T

(??? : T & { val x: Int }) match { ... }

I had some problems with the compiler output during tests in andtype-refinedtype-interaction.check - there must be spaces at the end of the broken lines, and my IDEA deletes those automatically on save. Would it be possible to increase the maximum line length?

@smarter
Copy link
Member

smarter commented Apr 6, 2017

Would it be possible to increase the maximum line length?

Possibly by changing the -pagewidth setting used to run tests, but maybe we could fix the output to not contain trailing spaces, or we could cheat and call trim on lines before comparing them in PatmatExhaustivityTest

@abgruszecki
Copy link
Contributor Author

The tests fail since I added an additional line of comment to tests/patmat/andtype-refinedtype-interaction.scala - I'll see if I can do something with the spaces-at-the-EOL while fixing this.

implementability(and) match {
case Unimplementable => Empty
case _ => Typ(and, true)
}
Copy link
Contributor

@liufengyun liufengyun Apr 7, 2017

Choose a reason for hiding this comment

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

By implementability, I guess you mean whether the greatest lower bound of tp1 and tp2 is possibly inhabited or not.

I'm wondering if there's some facility in Typer to soundly approximate GLB of two types? My intuition is that there should have some similar facilities, in order to simplify intersection types, but I don't know where it's located.

TypeComparer#glb is close, but it seems it does not use the knowledge of Final and single inheritance. Is it possible to refine TypeComparer#glb to take advantage of Final and single inheritance? Ping @smarter @odersky .

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought about it, but this is slightly different from inhabitation - I'm not interested in whether there exists a value of the given type, but rather whether it's possible to create a class or trait of this type. For instance, a singleton type is inhabited, but not implementable. The type being inhabited is a stronger statement, and I don't think it's necessary to check for it, since all types which are inhabited but not implementable will be caught by the subtyping check.

Checking for inhabitation would work too and I tried to look for an existing method, but I didn't find any. I'd be happy to use it if there's one.

@abgruszecki
Copy link
Contributor Author

implementability should be cleaner now - I concentrated on making it sound.
Atomic intersection now should only return an empty space if the space really
is empty. I've been able to catch an error in andtype-opentype-interaction
test thanks to this - previously sealed was incorrectly treated as impossible
to extend.

There are still some cases to be added to implementability, like type bounds.
I've tried adding a case for HKApply, but I've been unable to get a value of that.

I also looked at the methods in TypeComparer, but I don't think any of
them are particularly helpful - tp1 & tp2 could be used to potentially
simplify the types a bit, but I'd still have to traverse the type returned
by it and determine if there's a class collision. @smarter @odersky if you have
any suggestions regarding what methods could be useful here (possibly as a reference),
I'd be grateful.

As a sidenote - debug printing I've added is only temporary and I'll remove it
before creating a real pull request to avoid conflicts with #2197.

@liufengyun
Copy link
Contributor

@AleksanderBG Thanks for the good work. #2197 has been merged, there's some conflict, maybe you can resolve first?

@odersky will have a look at this PR and see if there's some existing facility in typer that can be used, or we keep the hack in the exhaustivity check.

@abgruszecki
Copy link
Contributor Author

Rebased to master & touched up the commit history.

Check if at least one of the types is a trait before deciding that
their intersection is non-empty.
Handle:
- nested and-types
- refined types
- type aliasing

Add line end trimming to patmat tests to work around an issue
where lines end with spaces.
For consistency with or-types.
@abgruszecki
Copy link
Contributor Author

@odersky when can I count on your review? I can improve this if needed,
but working on this has already cut a bit too much into my master's thesis,
so I don't want to do any unnecessary work.

@odersky
Copy link
Contributor

odersky commented Apr 26, 2017

I am not too familiar with Space, defer to @liufengyun for that. What I can see LGTM.

@liufengyun
Copy link
Contributor

OK, let's merge then. Thanks a lot @AleksanderBG .

@liufengyun liufengyun changed the title [WIP] Extend exhaustivity checks to and-types Extend exhaustivity checks to and-types Apr 26, 2017
@liufengyun liufengyun merged commit 8815b40 into scala:master Apr 26, 2017
@abgruszecki
Copy link
Contributor Author

Thank you too for you help @liufengyun!

@abgruszecki abgruszecki deleted the wip-andtype-exhaustivity branch April 26, 2017 18:04
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.

5 participants