-
Notifications
You must be signed in to change notification settings - Fork 1.1k
The singleton instance trick used by =:=
is unsound
#8430
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
Comments
Can we find a single case where inferring GADT constraints from singleton type checks is actually useful? Taking object identity into account when inferring GADT constraints sounds deeply bogus to me. As we can see, it also prevents useful optimizations such as reusing a single instance of |
I don't have a concrete example, but being able to infer constraints from equality sounds like something that'd be useful when writing theorem-prover-style code, maybe @Blaisorblade has an idea ?
Not at all. |
The typing rule makes some sense for objects with type members, tho I’m not sure I can think of a use case there; maybe when you match hlists against null? |
This has been discussed a long time ago in the scalac bug tracker, (and apparently it's the reason why polymorphic, non-specialized zero-arg givens aren't lazy vals by default?..) I can't find the link now but I mentioned it in
|
Surely, you could also avoid the cast by putting the appropriate @AleksanderBG
I'm sure there are many and we just haven't thought of them yet :^) |
Another way this could be fixed if by using type =:=[-From, +To]
type <:<[-From, +To] <: =:=[From, To]
erased implicit def refl[A]: A =:= A = ???
extension ops on [From, To](erased ev: =:=[From, To]) {
def apply(x: From): To = x.asInstanceOf[To]
} The upside (performance-wise and soundness-wise) and downside (usability-wise) is that all Note: the collective extension do not support erased yet but it should support it. |
@nicolasstucki what if people have code like this? def test[T: <:<[Int, *]](arr: ArrayBuffer[Int]): ArrayBuffer[T] = arr.map(summon) |
@LPTK what we could also do is add a |
@AleksanderBG It seems you never want to mark only one parameter as phantom (also, I'm not sure the name is good). It's a bit annoying that this annotation is specific for singleton type tests and singleton values casted to multiple types, but I'm not sure of an easy way to generalize it. OTOH, it seems none of the proposals can go in for 3.0 (except a compiler-hardcoded blacklist of types where GADT inference must be avoided), and I'm unsure which can go in for 3.1... |
@Blaisorblade This
It's just that we're approaching the issue from another angle, since in Haskell people wanted a safe way of coercing between values of two types, and what we want is not inferring GADT constraints from runtime instance checks on values which were "unsafely" coerced. |
What exactly makes the compiler assume that it can compile this to a runtime equality check instead of producing an unchecked erasure warning (and why would an opaque type avoid it)? Casting a single instance of an immutable and invariant class to whatever instance is required is commonplace in the standard library and in other Scala code. |
That's the semantics of
there's nothing that is left unchecked, so there's no warning
opaque types are not injectives like class types, if we have
Yes, and I wonder if we could get rid of this pattern. For |
FWIW, as I hinted earlier, this match semantics dates back to the paper introducing case classes in 2007 (http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.547.5623&rep=rep1&type=pdf). Check out Fig. 11 of that paper. |
|
Btw, that would be
(https://www.scala-lang.org/files/archive/spec/2.13/08-pattern-matching.html#type-patterns) |
In the example you're not only establishing |
Injectivity is precisely the property you need to conclude
No, you can achieve the same thing in other ways: type Foo[T] = String
("": Foo[Int]) eq ("": Foo[String])
implicitly[Foo[Int] =:= Foo[String]] Again, you can't conclude anything about the relationship between Int and String from these. |
It doesn't really answer my question but after thinking about it some more I agree. Even Still, the opaque type looks like a hack to me. It prevents |
The |
I think the bigger problem right now is HashMap.empty and similar things in the standard library. If we can't find a satisfying solution for those then we'll have to follow @AleksanderBG original proposal and stop inferring GADT bounds from type tests like those, in which case =:= might as well stay the way it is right now. |
Wouldn't a |
That seems weird to me since K is definitely not a phantom type parameter in a hashmap. |
also not sure how this phantom annotation would work with class hierarchies, especially since a HashMap[K, V] is an Iterable[(K, V)] as mentioned above |
I don't think And there exists one hashmap where they are phantoms; that's clearly enough. Same for the hypothetical "invariant
Argh, great point. Inference is unsound on
So, for soundness, if |
I'm in favor of making
The |
How about emitting an
|
I think the correct parallel to what you're proposing would be emitting an In both cases what this is really about is unsound uses of Of course, the "matching scrutinee types" use case is anecdotal in comparison to normal pattern matching, so it may still be okay to have the warning. |
The real issue here is not pattern matching, it is not GADT, it is not singleton types. The problem is the cast. As soon as you do a cast, you gave your right to soundness. We use theoretically invalid casts that are actually valid under erasure for efficiency. But that is at the expense of soundness. No amount of patching it up after the fact will truly fix the issue. |
@sjrd to recapture what was said before: the cast mimicks the way languages like Haskell reuse object instances when possible. @LPTK I'm kind of surprised you'd say that it "should be OK that you can upcast One final observation: we are 28 comments into this discussion, and I have not observed a single person give an example of inferring GADT constraints from singleton type checks being actually useful. Meanwhile, we've seen multiple potential makeshift solutions, and all of them have some holes. If nobody can find such an example, I'd be in favour of simply not inferring GADT constraints from such type checks. |
@AleksanderBG |
I guess GADT constraints from singletons could be useful whenever there's a GADT with object members: enum TimeUnit[T] {
def convert(t: Time): Time = this match {
case TMonth => t.truncateToMonth
case TDay => t.truncateToDay
}
case TMonth extends TimeUnit[Month]
case TDay extends TimeUnit[Day]
}
def convert[T](tu: TimeUnit[T], t: Time): T = tu match {
case TMonth => Month(tu.convert(t))
case TDay => Day(tu.convert(t))
) |
@neko-kai that's a good point, but in these cases it's definitely fine to infer constraints, since we're comparing with a literal. The example I would be looking for would check if two unknown objects (say, arguments to a function) are actually the same object, and then use the GADT constraints following from that. |
@AleksanderBG, there is a paper which makes good use of these constraints, which has already been cited twice by @Blaisorblade. Is there a reason you keep ignoring it? Let me paste the example below for you, since you seem to have trouble opening the PDF 😛 It's about encoding type-safe environments: abstract class Env {
def apply[a](v : Var[a]): a
def extend[a](v : Var[a], x : a) = new Env {
def apply[b](w : Var[b]): b = w match {
case : v.type => x // v eq w, hence a = b
case => Env.this.apply(w)
}
}
}
object empty extends Env {
def apply[a](x : Var[a]): a = throw new Error(”not found : ”+x.name)
}
// Evaluation :
def eval[a](t : Term[a], env : Env): a = t match {
...
case f : Lam[b, c] => (y : b) => eval(f.e, env.extend(f.x, y))
...
Do you have a specific example of unsoundness due to
There are plenty of things that ML languages do which make no sense in Scala. Like the way OCaml handles GADT reasoning based on type equalities, as you very well know! |
@LPTK indeed the paper does show an example! In the future, I shall try to read every paper linked in any thread which discusses what should we do if a user, with determination worthy of a better cause, tries to point a smoking gun at their own foot and blow it off - I think that's going to be a great approach! But seriously, @Blaisorblade did explicitly mention that the paper provided an example, and I glossed over that - 'scuse me. I guess that this means that "just stop inferring constraints from singleton type checks" solution also has a hole in it. To be more specific about my problem with |
@AleksanderBG @LPTK While discussions can be frustrating, I recommend avoiding such passive-aggressive sarcasm :-) It’d have been more effective if I pasted the example myself, since my remark needed two clarifications for two different and intelligent readers; thanks @LPTK for doing so. @AleksanderBG Instead of @phantom, maybe we should use some That could work because Scala 2 essentially didn’t have working GADTs, so it’s okay to require an annotation to enable them; and because it propagates in a better way — you can’t safely use these casts for any descendent of a @neko-kai The problem doesn’t affect Scala 2 the same way, because the compiler does give an unchecked warning. ClassCastException are bugs if they don’t involve code with unchecked warnings — and (usually) if they don’t involve casts (as @sjrd points out). But these casts have in fact been safe for a long time! And in fact I slightly disagree with @sjrd: under certain conditions you can formally prove that some well-behaved code using casts is in fact safe, as done by the RustBelt research project. We can’t do it for full Scala (and not anytime soon), but I’ve formalized in Coq two examples for a pDOT variant (and submitted a paper, preprint at https://iris-project.org/pdfs/2020-dot-submission.pdf; sorry to toot my own horn, but it’s relevant). Of course, you can’t allow both those examples and singleton inference at the same time; and allowing unsound examples is tricky business (you must use the same semantics). Last, my semantics cannot prove these casts sound yet; I first need support for HK-DOT. |
Since Scala 2 already produces an unchecked warning for such patterns then Dotty could sidestep the issue by doing that too. This won't be a regression compared to Scala 2, as an ordinary user I'd deem that acceptable. |
@Blaisorblade don't worry, @AleksanderBG and I are just joking (we're friends in real life). Yeah, implementing |
Yeah, I'd be up for unchecked + maybe marking this as stat:revisit. I'm sure more urgent bugs abound :-) |
unfortunately those bugs aren't as much fun to discuss :-) |
Given that 1) instance reuse tricks seem to be reasonably common (we've found at least two in stdlib) and 2) singleton type patterns that can cause the issue seem to be exceedingly rare, simply emitting a warning on all such patterns sounds good to me. |
In the standard library,
=:=
is defined as follow:I always thought this was perfectly fine, but thanks to Dotty's world-class GADT support, it isn't anymore :)
The same code also compiles and crashes in scalac, but produces an unchecked erasure warning which I believe is incorrect: if it weren't for the cast used to get an instance of
=:=
, this code would be perfectly fine.What can we do about this? In the short-term, either do nothing or special-case the GADT handling of
=:=
in the compiler. in the long-term, we should rewrite=:=
and<:<
using an opaque type to some dummy value:There's still a cast(EDIT: actually not needed), but the use of opaque types prevents the bogus inference of GADT bounds:https://github.com/lampepfl/dotty/blob/b448db2454e003f1448a31746daed11001a2ec86/compiler/src/dotty/tools/dotc/core/TypeComparer.scala#L869-L870
(another solution would be to somehow enforce that values of type
=:=
must always be erased at runtime, and therefore do not have a notion of reference equality)The text was updated successfully, but these errors were encountered: