-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Avoid interpolating to Nothing
in a special case
#16328
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
Conversation
If we have an selection `e.m` where e's type is a type variable T that does not have a lower bound in the current constraint, avoid interpolating `T` to nothing. Doing this would make the section ill-typed. By both interpolating `T` we leave the possibility open to instantiate it to its upper bound in couldInstantiateTypeVar.
Note: I tried to avoid interpolating to Nothing in general, but that gives test failures. |
I suppose it's asking too much to also avoid |
Sorry, that's probably what you meant by "in general", which you said gave test failures. I would have thought that with a non-trivial upper bound you could always instantiate to that instead of Nothing, but perhaps that's what you tried. |
In fact I am no longer sure this PR is a good idea. It's a very special case. We don't interpolate T to Nothing if
If we drop either of these side conditions, tests go wrong. It's unsatisfactory that
now works but
doesn't. Another shortcoming is that
still does not work, because here |
Co-authored-by: Guillaume Martres <[email protected]>
I don't really know what I'm doing here, but this one-line diff
makes both the tested case and val tmp = method(y)
tmp.member pass. It only fails two tests:
But I didn't really know how to debug from there. I looked a little at i864 and it seemed like |
This is already the case in various situations where we defer instantiation of a type variable in a prefix (e.g.,
@adampauls Could you share a concrete example of code you'd like to see typecheck? It's hard to think of a useful implementation of |
It's true that with erasure, |
But how do we define what the right thing is? If we care about having the largest amount of meaningful programs infer correctly, then we don't really need to worry about |
Of course I understand and agree. There are already several places in the code that check whether a lower bound is non-trivial before inferring Nothing and I suspect/hope that this is not any further complication, just an effort to make things consistent.
|
I was going to say that Nothing has all the methods, but your example proves that's not true. Makes me think whether there should be something like a lazy "Sub" type. As in, instead of lubbing A and B, we can defer with |
A more realistic example that still fails with the class Foo
trait Typeclass[T] {
def someString(x: T): String
}
given Typeclass[Foo] with {
def someString(x: Foo): String = "Foo impl"
}
class Bar(val foo: Foo)
given (Foo => Boolean) = _ => true
val latestTable: Map[scala.reflect.ClassTag[_], Seq[_]] = Map(summon[scala.reflect.ClassTag[Foo]] -> Seq(new Foo))
def typeclassConstrained[T](x: T)(using Typeclass[T]): String = summon[Typeclass[T]].someString(x)
def latest[T: scala.reflect.ClassTag](constraint: T => Boolean): T = {
latestTable(summon[scala.reflect.ClassTag[T]]).asInstanceOf[Seq[T]].filter(constraint).head
}
typeclassConstrained(latest((x: Foo) => true)) Works fine if |
Thanks for the example! This sort of pattern with ClassTag came up before, but I thought we handled it correctly. In fact, I had a very similar example 7 years ago (!) (#718 (comment)): import scala.reflect.ClassTag
import scala.reflect.classTag
object Test {
def getParamType[T: ClassTag](x: T => Int) = println(classTag[T])
def main(args: Array[String]): Unit = {
getParamType((x: Int) => x) // dotty: Nothing, scalac: Int
}
} But this example infers Int now as expected, so it'd be interesting to figure out what exactly is the critical difference between your example and this simplified one. |
A minimal example that reproduces the problem is: import scala.reflect.ClassTag
import scala.reflect.classTag
object Test {
def getParamType[T: ClassTag](x: T => Int): T = ???
def id[S](x: S): S = x
def main(args: Array[String]) = {
val x = id(getParamType((x: Int) => x))
val y: Int = x // error
}
}
Before doing the implicit search, we instantiate I'm not sure if there's anything we can change here that wouldn't break something else, unless our changes are specific to classtag synthesis. This could make sense for improved compatibility with Scala 2, but since ClassTag are already problematic in various ways (#1730, zio/zio#3136 (comment), #7554), so rather than adding more fragile special cases in inference, we might want to encourage replacements instead (e.g. usage of inline def/match). |
In which Scala version do you get an error? |
Maybe we can handle this via the
|
Do you really think a lot would break if |
Type variable are normally either instantiated to their lower bound (after some widening in `widenInferred`) or upper bound. This commit changes this behavior when the type variable instantiation is forced (which happens in particular before doing an implicit search, but also when typing the parameters of a lambda). We now use the "nonParam" bound instead where other type parameters have been stripped. For example in i16328.scala, when typing id(fromParam((x: Base) => x)) we end up with id[?S](getParamType[?T]((x: Int) => x)) where ?T <: ?S ?T <: Base Before this commit, ?T was then instantiated to ?S & Base (via Typer#adaptNoArgsImplicitMethod#instantiate), which lead to an implicit search failure since ?S was left unconstrained. With this commit, we instead instantiate ?T to its non-param bound `Base` and propagate this to `?S` itself: ?T := Base ?S >: Base The idea is that this leads to constraints being propagated in a more intuitive way (?S will now be instantiated to Base rather than Nothing or Any), however this also means that we're adding a whole new way of instantiating type variables for a seemingly limited number of usecases (see also the discussion starting at scala#16328 (comment)). It's not clear if this won't end up breaking as many things as it fixes, and it would increase our maintenance burden, so I recommend holding off on this change for now.
The previous commit ensures that `ClassTag[Int & Nothing]` doesn't compile anymore so this isn't a soundness issue anymore, but since intersections can arise out of type inference, it's worth adding a special-case here for convenience. In particular, this fixes scala#16328 (comment)
The previous commit ensures that `ClassTag[Int & Nothing]` doesn't compile anymore so this isn't a soundness issue anymore, but since intersections can arise out of type inference, it's worth adding a special-case here for convenience. In particular, this fixes scala#16328 (comment)
I tried out an alternative strategy for type variable instantiation in smarter@39b33f5 but I decided to not go forward with it since I wasn't sure the complexity increase was worth it. Instead, I've opened #16492 which special-cases ClassTag instantiation to deal with the examples we discussed. |
…ference failure (#16492) The first commit generalizes the fix for #1730, the second commit improves inference in a common situation to generate a valid ClassTag rather than emitting an error, fixing the example from #16328 (comment).
The previous commit ensures that `ClassTag[Int & Nothing]` doesn't compile anymore so this isn't a soundness issue anymore, but since intersections can arise out of type inference, it's worth adding a special-case here for convenience. In particular, this fixes scala#16328 (comment)
If we have an selection
e.m
where e's type is a type variable T that does not have a lower bound in the current constraint, avoid interpolatingT
to Nothing. Doing this would make the section ill-typed. Not interpolatingT
leaves the possibility open to instantiate it to its upper bound later in couldInstantiateTypeVar.Fixes #16323