-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Unsound implementation of singleton type matching #9359
Comments
Any suggestion how to solve this? |
Looks like this is compiled to |
Yes, the compiler generates the wrong check for this code. I don't have time to look into it ATM, but I could give it a go in about a week. |
|
Interesting, I hadn't seen that code. I think that logic is erroneous: we can only fallback on |
(and indeed, the testcase can be rewritten to |
actually, Scala 2 has the same behaviour (calls BoxesRunTime.equals) and warns about erasure: example2.scala:5: warning: abstract type pattern K[B] (the underlying of k1.type) is unchecked since it is eliminated by erasure
case k: k1.type => Some(k)
^
1 warning
|
Ah indeed, this is related to #8430 where we concluded that we could always emit such warnings like Scala 2 does, so I guess that would technically solve this issue too (but I'm not supper happy with the fact that adding/removing a |
this seems to work fine: if (tp.widen.derivesFrom(defn.AnyValClass))
singleton(tp).equal(tree)
else
tree.ensureConforms(defn.ObjectType).select(defn.Object_eq).appliedTo(singleton(tp).ensureConforms(defn.ObjectType)) and the generated code seems fine: public <K, A, B> Option<Object> foo(Object k0, Object k1) {
None$ none$;
Object object = k0;
if (object == k1) {
Object k = object;
none$ = Some$.MODULE$.apply(k);
} else {
none$ = None$.MODULE$;
}
return none$;
} |
We can't do that, because if you have a primitive (or value class) upcasted to Any, it should not be compared using |
Isn't there a way to test, at runtime, whether two |
For primitives, you could hardcode a list, but for non-primitive value classes I don't see a way. As always the answer is: valhalla will solve everything :). |
Note that scala> class Test(val x: Int) extends AnyVal
// defined class Test
scala> val t = new Test(1)
val t: Test = 1
scala> val u = new Test(1)
val u: Test = 1
scala> t match { case x: u.type => true }
val res0: Boolean = true Now, |
Not sure I follow, you can't know at runtime whether something is a boxed AnyVal or not, and you can't prevent |
For a value class with type parameters you'd need unchecked warnings yeah. |
It would be helpful if |
You would not necessarily even see that the base thing has type parameters! def test(x: Any, y: Any) = x match { case y: x.type => } (Now, maybe the above could get an unchecked warning. Currently it doesn't raise anything.) |
The design of value classes is frozen until valhalla arrives since that will change everything (and it's not like we can change it now with the Scala 2 compat story anyway). We just need unchecked warnings as already decided in #8430. |
To be clear, you're proposing to basically remove type system support for singleton type patterns? (i.e., one can always use |
I'm not proposing anything but that was the consensus yeah. |
So if I understand the issue correctly, it seems that with a pattern like
@smarter do I understand correctly that we're effectively forced to pick 1) because of previous value class design? I'm not entirely certain if that choice sits with me well. Reg. #8430 - I was hoping that we could just emit a warning along the lines of "singleton type patterns are unsound with the common instance reuse trick, add annotation X to assert that you know what you're doing". Given this issue, that may not be enough though. |
Are opaque types immune from the same issue with value classes? They can have different type parameters while sharing the exact same identity, just as AnyVals do. |
@neko-kai opaque types don't have this problem because they don't box things implicitly — which is what messes with object identity semantics, leading to the paradoxes we're grappling with here. That said, if because of value classes we decided to keep following the crazy scheme that |
@LPTK opaque types arguably have an even worse problem precisely because they don't box. Consider:
Here neither I also wouldn't be so quick to call the value class scheme "crazy" - I meant it when I said that I'm not sure what to prefer between letting wrong values match the pattern and letting good values not match the pattern. Arguably both are unsound, the second will just fail early (b/c of unmatched value error). |
@AleksanderBG
I don't think that poses any problem. What makes you think that?
At least in principle, we can tell whether two
What's crazy is that this scheme is not restricted to value classes! The current scheme will call any user-defined scala> def foo[A](x: A, y: A) = x match { case _: y.type => 0 }
def foo[A](x: A, y: A): Unit
scala> class T { override def equals(that: Any) = { println("hello!"); true } }
// defined class T
scala> foo(new T, new T)
hello!
val res0: Int = 0 |
Why wouldn't type parameters of opaque types participate in GADT reasoning? They are no different from any other "phantom" type parameter. I think the precise thing that we have going here is that singleton type patterns which are subtypes of opaque types are simply unsound, and so they can result in unsound GADT constraints. |
If type parameters of both opaques and AnyVals wouldn't participate in GADT reasoning, would that avoid the issue? |
@AleksanderBG I'm surprised that you say that. Maybe we're misunderstanding each other. Opaque type parameters are very different from class type parameters (phantom or not). Consider: class Test {
type F[T]
}
val test: Test = ... You obviously cannot treat Now, from the type system's point of view, opaque types should behave like abstract type members. (The major difference is that opaque types are erased to their underlying types, whereas abstract type members are erased to |
@LPTK ah right, I see what you're objecting to. Yes, you're correct, opaque type constructors aren't injective, which makes my point moot. I mostly focused on the "not participate in GADT reasoning" bit, because to me that suggests that opaque type constructors are some sort of special case for GADT reasoning, which I don't think they are. This actually brings us to the point made by @neko-kai - perhaps we should treat value classes as not injective? This'd solve the issue (at least it'd get rid of the unsoundness), and the implementation should be trivial. However, unless I'm thinking about this wrong, I don't think this is the correct thing to do - if |
You'd still have to do something about type members, at least. Would that even be sufficient? Who knows? That doesn't seem like a very good solution.
Currently we can, because of the bogus So I think we should disallow We can't even make |
@LPTK an opaque type can also be a subtype of I agree that only permitting typecase patterns when we know we're sound is the principled solution. This'd be a compatibility break with Scala 2 though, which simply emitted a warning and compiled such patterns. I'm not certain if this is necessarily a good thing right now. |
What? Look, the situation is really simple:
First, |
Right, I guess I keep making the mistake of thinking of opaque types like they were newtypes, whereas they really are just type members. By "we get a value of type |
would it be too intrusive to require some implicit evidence that implements singleton type testing that is only required when the type is upper bound Any? |
As a first guess, I'd expect so. Remember that this implicit evidence would need to be passed all the way from where we know the concrete type, which might be separated by multiple methods where the type is abstract (a type parameter). |
Minimized code
https://scastie.scala-lang.org/2LlP0dwdT7qqFiTTGUSszA
Output
Expectation
Type error.
The text was updated successfully, but these errors were encountered: