-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Tweaks to given priority #21305
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
Tweaks to given priority #21305
Changes from 3 commits
bdfb12e
3cf4ada
2795a50
bba0db3
462b293
3d9ce8e
62acaf1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
+4 −4 | compat/src/test/scala/test/scala/collection/BuildFromTest.scala |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1830,15 +1830,13 @@ trait Applications extends Compatibility { | |
isAsGood(alt1, tp1.instantiate(tparams.map(_.typeRef)), alt2, tp2) | ||
} | ||
case _ => // (3) | ||
def compareValues(tp1: Type, tp2: Type)(using Context) = | ||
isAsGoodValueType(tp1, tp2, alt1.symbol.is(Implicit), alt2.symbol.is(Implicit)) | ||
tp2 match | ||
case tp2: MethodType => true // (3a) | ||
case tp2: PolyType if tp2.resultType.isInstanceOf[MethodType] => true // (3a) | ||
case tp2: PolyType => // (3b) | ||
explore(compareValues(tp1, instantiateWithTypeVars(tp2))) | ||
explore(isAsGoodValueType(tp1, instantiateWithTypeVars(tp2))) | ||
case _ => // 3b) | ||
compareValues(tp1, tp2) | ||
isAsGoodValueType(tp1, tp2) | ||
} | ||
|
||
/** Test whether value type `tp1` is as good as value type `tp2`. | ||
|
@@ -1868,15 +1866,14 @@ trait Applications extends Compatibility { | |
* for overloading resolution (when `preferGeneral is false), and the opposite relation | ||
* `U <: T` or `U convertible to `T` for implicit disambiguation between givens | ||
* (when `preferGeneral` is true). For old-style implicit values, the 3.4 behavior is kept. | ||
* If one of the alternatives is an implicit and the other is a given (or an extension), the implicit loses. | ||
* | ||
* - In Scala 3.5 and Scala 3.6-migration, we issue a warning if the result under | ||
* Scala 3.6 differ wrt to the old behavior up to 3.5. | ||
* | ||
* Also and only for given resolution: If a compared type refers to a given or its module class, use | ||
* the intersection of its parent classes instead. | ||
*/ | ||
def isAsGoodValueType(tp1: Type, tp2: Type, alt1IsImplicit: Boolean, alt2IsImplicit: Boolean)(using Context): Boolean = | ||
def isAsGoodValueType(tp1: Type, tp2: Type)(using Context): Boolean = | ||
val oldResolution = ctx.mode.is(Mode.OldImplicitResolution) | ||
if !preferGeneral || Feature.migrateTo3 && oldResolution then | ||
// Normal specificity test for overloading resolution (where `preferGeneral` is false) | ||
|
@@ -1892,10 +1889,7 @@ trait Applications extends Compatibility { | |
val tp1p = prepare(tp1) | ||
val tp2p = prepare(tp2) | ||
|
||
if Feature.sourceVersion.isAtMost(SourceVersion.`3.4`) | ||
|| oldResolution | ||
|| alt1IsImplicit && alt2IsImplicit | ||
then | ||
if Feature.sourceVersion.isAtMost(SourceVersion.`3.4`) || oldResolution then | ||
// Intermediate rules: better means specialize, but map all type arguments downwards | ||
// These are enabled for 3.0-3.5, and for all comparisons between old-style implicits, | ||
// and in 3.5 and 3.6-migration when we compare with previous rules. | ||
|
@@ -1909,9 +1903,8 @@ trait Applications extends Compatibility { | |
case _ => mapOver(t) | ||
(flip(tp1p) relaxed_<:< flip(tp2p)) || viewExists(tp1, tp2) | ||
else | ||
// New rules: better means generalize, givens (and extensions) always beat implicits | ||
if alt1IsImplicit != alt2IsImplicit then alt2IsImplicit | ||
else (tp2p relaxed_<:< tp1p) || viewExists(tp2, tp1) | ||
// New rules: better means generalize | ||
(tp2p relaxed_<:< tp1p) || viewExists(tp2, tp1) | ||
end isAsGoodValueType | ||
|
||
/** Widen the result type of synthetic given methods from the implementation class to the | ||
|
@@ -1970,8 +1963,9 @@ trait Applications extends Compatibility { | |
else if winsPrefix1 then 1 | ||
else -1 | ||
|
||
val ownerScore = compareOwner(alt1.symbol.maybeOwner, alt2.symbol.maybeOwner) | ||
|
||
def compareWithTypes(tp1: Type, tp2: Type) = | ||
val ownerScore = compareOwner(alt1.symbol.maybeOwner, alt2.symbol.maybeOwner) | ||
val winsType1 = isAsGood(alt1, tp1, alt2, tp2) | ||
val winsType2 = isAsGood(alt2, tp2, alt1, tp1) | ||
|
||
|
@@ -1982,15 +1976,27 @@ trait Applications extends Compatibility { | |
// alternatives are the same after following ExprTypes, pick one of them | ||
// (prefer the one that is not a method, but that's arbitrary). | ||
if alt1.widenExpr =:= alt2 then -1 else 1 | ||
else ownerScore match | ||
case 1 => if winsType1 || !winsType2 then 1 else 0 | ||
case -1 => if winsType2 || !winsType1 then -1 else 0 | ||
case 0 => | ||
if winsType1 != winsType2 then if winsType1 then 1 else -1 | ||
else if alt1.symbol == alt2.symbol then comparePrefixes | ||
else 0 | ||
else | ||
ownerScore match | ||
case 1 => if winsType1 || !winsType2 then 1 else 0 | ||
case -1 => if winsType2 || !winsType1 then -1 else 0 | ||
case 0 => | ||
if winsType1 != winsType2 then if winsType1 then 1 else -1 | ||
else if alt1.symbol == alt2.symbol then comparePrefixes | ||
else 0 | ||
end compareWithTypes | ||
|
||
// For implicit resolution, take ownerscore as more significant than type resolution | ||
// Reason: People use owner hierarchies to explicitly prioritize, we should not | ||
// break that by changing implicit priority of types. On the other hand, we do | ||
// want to exhaust all other possibilities before using owner score as a tie breaker. | ||
// For instance, pos/scala-uri.scala depends on that. | ||
def drawOrOwner = | ||
if preferGeneral && !ctx.mode.is(Mode.OldImplicitResolution) then | ||
//println(i"disambi compare($alt1, $alt2)? $ownerScore") | ||
ownerScore | ||
else 0 | ||
|
||
if alt1.symbol.is(ConstructorProxy) && !alt2.symbol.is(ConstructorProxy) then -1 | ||
else if alt2.symbol.is(ConstructorProxy) && !alt1.symbol.is(ConstructorProxy) then 1 | ||
else | ||
|
@@ -2000,11 +2006,12 @@ trait Applications extends Compatibility { | |
val strippedType2 = stripImplicit(fullType2) | ||
|
||
val result = compareWithTypes(strippedType1, strippedType2) | ||
if (result != 0) result | ||
else if (strippedType1 eq fullType1) | ||
if (strippedType2 eq fullType2) 0 // no implicits either side: its' a draw | ||
if result != 0 then result | ||
else if strippedType1 eq fullType1 then | ||
if strippedType2 eq fullType2 | ||
then drawOrOwner // no implicits either side: its' a draw | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do We may also want to use the owner as the final tie breaker in the case where: we have an ambiguity with the strippedTypes, and that both alternatives have implicit parameters, and that the fullTypes are still ambiguous. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It turns out there are counter-examples for both schemes. If we do |
||
else 1 // prefer 1st alternative with no implicits | ||
else if (strippedType2 eq fullType2) -1 // prefer 2nd alternative with no implicits | ||
else if strippedType2 eq fullType2 then -1 // prefer 2nd alternative with no implicits | ||
odersky marked this conversation as resolved.
Show resolved
Hide resolved
|
||
else compareWithTypes(fullType1, fullType2) // continue by comparing implicits parameters | ||
} | ||
end compare | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -549,6 +549,12 @@ object Implicits: | |
/** An ambiguous implicits failure */ | ||
class AmbiguousImplicits(val alt1: SearchSuccess, val alt2: SearchSuccess, val expectedType: Type, val argument: Tree, val nested: Boolean = false) extends SearchFailureType: | ||
|
||
private[Implicits] var priorityChangeWarning: Message | Null = null | ||
|
||
def priorityChangeWarningNote(using Context): String = | ||
if priorityChangeWarning != null then s"\n\nNote: $priorityChangeWarning" | ||
else "" | ||
|
||
def msg(using Context): Message = | ||
var str1 = err.refStr(alt1.ref) | ||
var str2 = err.refStr(alt2.ref) | ||
|
@@ -1330,10 +1336,13 @@ trait Implicits: | |
if alt1.ref eq alt2.ref then 0 | ||
else if alt1.level != alt2.level then alt1.level - alt2.level | ||
else | ||
var cmp = comp(using searchContext()) | ||
lazy val prev = comp(using searchContext().addMode(Mode.OldImplicitResolution)) | ||
val cmp = comp(using searchContext()) match | ||
// if we get an ambiguity with new rules for a pair of old-style implicits, fall back to old rules | ||
case 0 if alt1.ref.symbol.is(Implicit) && alt2.ref.symbol.is(Implicit) => prev | ||
case cmp => cmp | ||
val sv = Feature.sourceVersion | ||
if isWarnPriorityChangeVersion(sv) then | ||
val prev = comp(using searchContext().addMode(Mode.OldImplicitResolution)) | ||
if disambiguate && cmp != prev then | ||
def warn(msg: Message) = | ||
val critical = alt1.ref :: alt2.ref :: Nil | ||
|
@@ -1345,13 +1354,21 @@ trait Implicits: | |
case _ => "none - it's ambiguous" | ||
if sv.stable == SourceVersion.`3.5` then | ||
warn( | ||
em"""Given search preference for $pt between alternatives ${alt1.ref} and ${alt2.ref} will change | ||
em"""Given search preference for $pt between alternatives | ||
| ${alt1.ref} | ||
|and | ||
| ${alt2.ref} | ||
|will change. | ||
|Current choice : ${choice(prev)} | ||
|New choice from Scala 3.6: ${choice(cmp)}""") | ||
prev | ||
else | ||
warn( | ||
em"""Change in given search preference for $pt between alternatives ${alt1.ref} and ${alt2.ref} | ||
em"""Given search preference for $pt between alternatives | ||
| ${alt1.ref} | ||
|and | ||
| ${alt2.ref} | ||
|has changed. | ||
|Previous choice : ${choice(prev)} | ||
|New choice from Scala 3.6: ${choice(cmp)}""") | ||
cmp | ||
|
@@ -1612,6 +1629,12 @@ trait Implicits: | |
val result = rank(sort(eligible), NoMatchingImplicitsFailure, Nil) | ||
for (critical, msg) <- priorityChangeWarnings do | ||
if result.found.exists(critical.contains(_)) then | ||
result match | ||
case result: SearchFailure => | ||
result.reason match | ||
case ambi: AmbiguousImplicits => ambi.priorityChangeWarning = msg | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch that these used to get swallowed up! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point. We should make a List. |
||
case _ => | ||
case _ => | ||
report.warning(msg, srcPos) | ||
result | ||
end searchImplicit | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
public enum JavaEnum { ABC, DEF, GHI } |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
//> using options -source 3.6-migration | ||
import scala.deriving.Mirror | ||
import scala.compiletime.* | ||
import scala.reflect.ClassTag | ||
import scala.annotation.implicitNotFound | ||
|
||
|
||
trait TSType[T] | ||
object TSType extends DefaultTSTypes with TSTypeMacros | ||
|
||
trait TSNamedType[T] extends TSType[T] | ||
|
||
trait DefaultTSTypes extends JavaTSTypes | ||
trait JavaTSTypes { | ||
given javaEnumTSType[E <: java.lang.Enum[E]: ClassTag]: TSNamedType[E] = ??? | ||
} | ||
object DefaultTSTypes extends DefaultTSTypes | ||
trait TSTypeMacros { | ||
inline given [T: Mirror.Of]: TSType[T] = derived[T] | ||
inline def derived[T](using m: Mirror.Of[T]): TSType[T] = { | ||
val elemInstances = summonAll[m.MirroredElemTypes] | ||
??? | ||
} | ||
|
||
private inline def summonAll[T <: Tuple]: List[TSType[_]] = { | ||
inline erasedValue[T] match { | ||
case _: EmptyTuple => Nil | ||
case _: (t *: ts) => summonInline[TSType[t]] :: summonAll[ts] | ||
} | ||
} | ||
} | ||
|
||
@main def Test = summon[TSType[JavaEnum]] // error |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
|
||
trait Foo[-T] | ||
trait Bar[-T] extends Foo[T] | ||
|
||
object Test { | ||
|
||
locally: | ||
implicit val fa: Foo[Int] = ??? | ||
implicit val ba: Bar[Int] = ??? | ||
summon[Foo[Int]] // ok | ||
|
||
locally: | ||
implicit val fa: Foo[Int] = ??? | ||
implicit val ba: Bar[Any] = ??? | ||
summon[Foo[Int]] // ok | ||
|
||
locally: | ||
given fa: Foo[Any] = ??? | ||
given ba: Bar[Int] = ??? | ||
summon[Foo[Int]] // error: now ambiguous, | ||
// was resolving to `ba` when using intermediate rules: | ||
// better means specialize, but map all type arguments downwards | ||
|
||
locally: | ||
implicit val fa: Foo[Any] = ??? | ||
implicit val ba: Bar[Int] = ??? | ||
summon[Foo[Int]] // is OK since we fall back to old rules for old-style implicits as a tie breaker | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
/* These tests show various mechanisms available for implicit prioritization. | ||
*/ | ||
import language.`3.6` | ||
|
||
class A // The type for which we infer terms below | ||
class B extends A | ||
|
||
/* First, two schemes that require a pre-planned architecture for how and | ||
* where given instances are defined. | ||
* | ||
* Traditional scheme: prioritize with location in class hierarchy | ||
*/ | ||
class LowPriorityImplicits: | ||
given g1: A() | ||
|
||
object NormalImplicits extends LowPriorityImplicits: | ||
given g2: B() | ||
|
||
def test1 = | ||
import NormalImplicits.given | ||
val x = summon[A] | ||
val _: B = x | ||
val y = summon[B] | ||
val _: B = y |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
public enum JavaEnum { ABC, DEF, GHI } |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import scala.deriving.Mirror | ||
import scala.compiletime.* | ||
import scala.reflect.ClassTag | ||
import scala.annotation.implicitNotFound | ||
|
||
|
||
trait TSType[T] | ||
object TSType extends DefaultTSTypes with TSTypeMacros | ||
|
||
trait TSNamedType[T] extends TSType[T] | ||
|
||
trait DefaultTSTypes extends JavaTSTypes | ||
trait JavaTSTypes { | ||
given javaEnumTSType[E <: java.lang.Enum[E]: ClassTag]: TSType[E] = ??? | ||
} | ||
object DefaultTSTypes extends DefaultTSTypes | ||
trait TSTypeMacros { | ||
inline given [T: Mirror.Of]: TSType[T] = derived[T] | ||
inline def derived[T](using m: Mirror.Of[T]): TSType[T] = { | ||
val elemInstances = summonAll[m.MirroredElemTypes] | ||
??? | ||
} | ||
|
||
private inline def summonAll[T <: Tuple]: List[TSType[_]] = { | ||
inline erasedValue[T] match { | ||
case _: EmptyTuple => Nil | ||
case _: (t *: ts) => summonInline[TSType[t]] :: summonAll[ts] | ||
} | ||
} | ||
} | ||
|
||
@main def Test = summon[TSType[JavaEnum]] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
public enum JavaEnum { ABC, DEF, GHI } |
Uh oh!
There was an error while loading. Please reload this page.