Skip to content

Commit adec842

Browse files
committed
Reorganize Eq checking
Two main changes: - We treat `Eq` as coherent, i.e Eq searches are never ambiguous. Instead, the first found instance is returned. This would be compatible with an eventual user-declarable coherence construct, but does not require it. - Instead of invalidating found `eqAny` instances after the fact, we turn things around: `eqAny` is no longer defined `implicit`, but can nevertheless me synthesized for validEqAny` args, similar to how `ClassTag` is synthesized. This simplifies the logic and leads to better debugability of implicit searches.
1 parent 9ca4c16 commit adec842

File tree

8 files changed

+100
-68
lines changed

8 files changed

+100
-68
lines changed

compiler/src/dotty/tools/dotc/core/Definitions.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,6 @@ class Definitions {
347347

348348
def DottyPredefModule(implicit ctx: Context) = DottyPredefModuleRef.symbol
349349

350-
def Predef_eqAny(implicit ctx: Context) = DottyPredefModule.requiredMethod(nme.eqAny)
351350
lazy val Predef_ImplicitConverterR = DottyPredefModule.requiredClass("ImplicitConverter").typeRef
352351
def Predef_ImplicitConverter(implicit ctx: Context) = Predef_ImplicitConverterR.symbol
353352

@@ -574,6 +573,8 @@ class Definitions {
574573
def EqClass(implicit ctx: Context) = EqType.symbol.asClass
575574
def EqModule(implicit ctx: Context) = EqClass.companionModule
576575

576+
def Eq_eqAny(implicit ctx: Context) = EqModule.requiredMethod(nme.eqAny)
577+
577578
lazy val XMLTopScopeModuleRef = ctx.requiredModuleRef("scala.xml.TopScope")
578579

579580
// Annotation base classes

compiler/src/dotty/tools/dotc/core/StdNames.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,7 @@ object StdNames {
502502
val staticClass : N = "staticClass"
503503
val staticModule : N = "staticModule"
504504
val staticPackage : N = "staticPackage"
505+
val strictEquality: N = "strictEquality"
505506
val synchronized_ : N = "synchronized"
506507
val tag: N = "tag"
507508
val tail: N = "tail"

compiler/src/dotty/tools/dotc/typer/Implicits.scala

Lines changed: 57 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -545,29 +545,49 @@ trait Implicits { self: Typer =>
545545
/** If `formal` is of the form ClassTag[T], where `T` is a class type,
546546
* synthesize a class tag for `T`.
547547
*/
548-
def synthesizedClassTag(formal: Type, pos: Position)(implicit ctx: Context): Tree = {
549-
if (formal.isRef(defn.ClassTagClass))
550-
formal.argTypes match {
551-
case arg :: Nil =>
552-
fullyDefinedType(arg, "ClassTag argument", pos) match {
553-
case defn.ArrayOf(elemTp) =>
554-
val etag = inferImplicitArg(defn.ClassTagType.appliedTo(elemTp), error, pos)
555-
if (etag.isEmpty) etag else etag.select(nme.wrap)
556-
case tp if hasStableErasure(tp) =>
557-
if (defn.isBottomClass(tp.typeSymbol))
558-
error(where => i"attempt to take ClassTag of undetermined type for $where")
559-
ref(defn.ClassTagModule)
560-
.select(nme.apply)
561-
.appliedToType(tp)
562-
.appliedTo(clsOf(erasure(tp)))
563-
.withPos(pos)
564-
case tp =>
565-
EmptyTree
566-
}
567-
case _ =>
568-
EmptyTree
569-
}
570-
else EmptyTree
548+
def synthesizedClassTag(formal: Type)(implicit ctx: Context): Tree =
549+
formal.argTypes match {
550+
case arg :: Nil =>
551+
fullyDefinedType(arg, "ClassTag argument", pos) match {
552+
case defn.ArrayOf(elemTp) =>
553+
val etag = inferImplicitArg(defn.ClassTagType.appliedTo(elemTp), error, pos)
554+
if (etag.isEmpty) etag else etag.select(nme.wrap)
555+
case tp if hasStableErasure(tp) =>
556+
if (defn.isBottomClass(tp.typeSymbol))
557+
error(where => i"attempt to take ClassTag of undetermined type for $where")
558+
ref(defn.ClassTagModule)
559+
.select(nme.apply)
560+
.appliedToType(tp)
561+
.appliedTo(clsOf(erasure(tp)))
562+
.withPos(pos)
563+
case tp =>
564+
EmptyTree
565+
}
566+
case _ =>
567+
EmptyTree
568+
}
569+
570+
/** If `formal` is of the form Eq[T, U], where no `Eq` instance exists for
571+
* either `T` or `U`, synthesize `Eq.eqAny[T, U]` as solution.
572+
*/
573+
def synthesizedEq(formal: Type)(implicit ctx: Context): Tree = {
574+
//println(i"synth eq $formal / ${formal.argTypes}%, %")
575+
formal.argTypes match {
576+
case args @ (arg1 :: arg2 :: Nil)
577+
if !ctx.featureEnabled(defn.LanguageModuleClass, nme.strictEquality) &&
578+
validEqAnyArgs(arg1, arg2)(ctx.fresh.setExploreTyperState) =>
579+
ref(defn.Eq_eqAny).appliedToTypes(args).withPos(pos)
580+
case _ =>
581+
EmptyTree
582+
}
583+
}
584+
585+
def hasEq(tp: Type): Boolean =
586+
inferImplicit(defn.EqType.appliedTo(tp, tp), EmptyTree, pos).isInstanceOf[SearchSuccess]
587+
588+
def validEqAnyArgs(tp1: Type, tp2: Type)(implicit ctx: Context) = {
589+
List(tp1, tp2).foreach(fullyDefinedType(_, "eqAny argument", pos))
590+
assumedCanEqual(tp1, tp2) || !hasEq(tp1) && !hasEq(tp2)
571591
}
572592

573593
/** The context to be used when resolving a by-name implicit argument.
@@ -606,7 +626,13 @@ trait Implicits { self: Typer =>
606626
error(where => s"ambiguous implicits: ${ambi.explanation} of $where")
607627
EmptyTree
608628
case failure: SearchFailure =>
609-
val arg = synthesizedClassTag(formalValue, pos)
629+
val arg =
630+
if (formalValue.isRef(defn.ClassTagClass))
631+
synthesizedClassTag(formalValue)
632+
else if (formalValue.isRef(defn.EqClass))
633+
synthesizedEq(formalValue)
634+
else
635+
EmptyTree
610636
if (!arg.isEmpty) arg
611637
else {
612638
var msgFn = (where: String) =>
@@ -638,10 +664,10 @@ trait Implicits { self: Typer =>
638664
}
639665

640666
val lift = new TypeMap {
641-
def apply(t: Type) = t match {
667+
def apply(t: Type): Type = t match {
642668
case t: TypeRef =>
643669
t.info match {
644-
case TypeBounds(lo, hi) if lo ne hi => hi
670+
case TypeBounds(lo, hi) if lo ne hi => apply(hi)
645671
case _ => t
646672
}
647673
case _ =>
@@ -714,6 +740,8 @@ trait Implicits { self: Typer =>
714740
if (argument.isEmpty) f(resultType) else ViewProto(f(argument.tpe.widen), f(resultType))
715741
// Not clear whether we need to drop the `.widen` here. All tests pass with it in place, though.
716742

743+
private def isCoherent = pt.isRef(defn.EqClass)
744+
717745
assert(argument.isEmpty || argument.tpe.isValueType || argument.tpe.isInstanceOf[ExprType],
718746
em"found: $argument: ${argument.tpe}, expected: $pt")
719747

@@ -761,40 +789,16 @@ trait Implicits { self: Typer =>
761789
case _ => false
762790
}
763791
}
764-
// Does there exist an implicit value of type `Eq[tp, tp]`
765-
// which is different from `eqAny`?
766-
def hasEq(tp: Type): Boolean = {
767-
def search(contextual: Boolean): Boolean =
768-
new ImplicitSearch(defn.EqType.appliedTo(tp, tp), EmptyTree, pos)
769-
.bestImplicit(contextual) match {
770-
case result: SearchSuccess =>
771-
result.ref.symbol != defn.Predef_eqAny ||
772-
contextual && search(contextual = false)
773-
case result: AmbiguousImplicits => true
774-
case _ => false
775-
}
776-
search(contextual = true)
777-
}
778792

779-
def validEqAnyArgs(tp1: Type, tp2: Type) = {
780-
List(tp1, tp2).foreach(fullyDefinedType(_, "eqAny argument", pos))
781-
assumedCanEqual(tp1, tp2) || !hasEq(tp1) && !hasEq(tp2) ||
782-
{ implicits.println(i"invalid eqAny[$tp1, $tp2]"); false }
783-
}
784793
if (ctx.reporter.hasErrors)
785794
nonMatchingImplicit(ref, ctx.reporter.removeBufferedMessages)
786795
else if (contextual && !ctx.mode.is(Mode.ImplicitShadowing) &&
787796
!shadowing.tpe.isError && !refSameAs(shadowing)) {
788797
implicits.println(i"SHADOWING $ref in ${ref.termSymbol.owner} is shadowed by $shadowing in ${shadowing.symbol.owner}")
789798
shadowedImplicit(ref, methPart(shadowing).tpe)
790799
}
791-
else generated1 match {
792-
case TypeApply(fn, targs @ (arg1 :: arg2 :: Nil))
793-
if fn.symbol == defn.Predef_eqAny && !validEqAnyArgs(arg1.tpe, arg2.tpe) =>
794-
nonMatchingImplicit(ref, Nil)
795-
case _ =>
796-
SearchSuccess(generated1, ref, cand.level, ctx.typerState)
797-
}
800+
else
801+
SearchSuccess(generated1, ref, cand.level, ctx.typerState)
798802
}}
799803

800804
/** Given a list of implicit references, produce a list of all implicit search successes,
@@ -812,7 +816,7 @@ trait Implicits { self: Typer =>
812816
case fail: SearchFailure =>
813817
rankImplicits(pending1, acc)
814818
case best: SearchSuccess =>
815-
if (ctx.mode.is(Mode.ImplicitExploration)) best :: Nil
819+
if (ctx.mode.is(Mode.ImplicitExploration) || isCoherent) best :: Nil
816820
else {
817821
val newPending = pending1.filter(cand1 =>
818822
isAsGood(cand1.ref, best.ref, cand1.level, best.level)(nestedContext.setExploreTyperState))

library/src/dotty/DottyPredef.scala

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,9 @@ package dotty
22

33
import scala.reflect.ClassTag
44
import scala.Predef.???
5-
import scala.collection.Seq
65

7-
/** unimplemented implicit for TypeTag */
86
object DottyPredef {
97

10-
/** A fall-back implicit to compare values of any types.
11-
* The compiler will restrict implicit instances of `eqAny`. An instance
12-
* `eqAny[T, U]` is _valid_ if `T <: U` or `U <: T` or both `T` and `U` are
13-
* Eq-free. A type `S` is Eq-free if there is no implicit instance of `Eq[S, S]`.
14-
* An implicit search will fail instead of returning an invalid `eqAny` instance.
15-
* The method is here instead of the `Eq` object so that it can be disabled.
16-
*/
17-
implicit def eqAny[L, R]: Eq[L, R] = Eq
18-
198
/** A class for implicit values that can serve as implicit conversions
209
* The implicit resolution algorithm will act as if there existed
2110
* the additional implicit definition:

library/src/scala/Eq.scala

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package scala
22

33
import annotation.implicitNotFound
4+
import scala.collection.{GenSeq, Set}
45

56
/** A marker trait indicating that values of type `L` can be compared to values of type `R`. */
67
@implicitNotFound("Values of types ${L} and ${R} cannot be compared with == or !=")
@@ -12,6 +13,14 @@ sealed trait Eq[-L, -R]
1213
*/
1314
object Eq extends Eq[Any, Any] {
1415

16+
/** A fall-back "implicit" to compare values of any types.
17+
* Even though this method is not declared implicit, the compiler will
18+
* compute instances as solutions to `Eq[T, U]` queries if `T <: U` or `U <: T`
19+
* or both `T` and `U` are Eq-free. A type `S` is Eq-free if there is no
20+
* implicit instance of type `Eq[S, S]`.
21+
*/
22+
def eqAny[L, R]: Eq[L, R] = Eq
23+
1524
// Instances of `Eq` for common types
1625

1726
implicit def eqNumber : Eq[Number, Number] = Eq
@@ -29,7 +38,8 @@ object Eq extends Eq[Any, Any] {
2938
// true asymmetry, modeling the (somewhat problematic) nature of equals on Proxies
3039
implicit def eqProxy : Eq[Proxy, Any] = Eq
3140

32-
implicit def eqSeq[T, U](implicit eq: Eq[T, U]): Eq[Seq[T], Seq[U]] = Eq
41+
implicit def eqSeq[T, U](implicit eq: Eq[T, U]): Eq[GenSeq[T], GenSeq[U]] = Eq
42+
implicit def eqSet[T, U](implicit eq: Eq[T, U]): Eq[Set[T], Set[U]] = Eq
3343

3444
implicit def eqByteNum : Eq[Byte, Number] = Eq
3545
implicit def eqNumByte : Eq[Number, Byte] = Eq

library/src/scalaShadowing/language.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,4 +195,7 @@ object language {
195195

196196
/** Where imported, auto-tupling is disabled */
197197
object noAutoTupling
198+
199+
/* Where imported loose equality using eqAny is disabled */
200+
object strictEquality
198201
}

tests/neg/equality.scala

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ object equality {
5656
null == Str("x")
5757
null == null
5858

59+
1 == true // error
60+
61+
null == true // OK by eqProxy
62+
true == null // error
63+
null == 1 // OK by eqProxy or eqNumInt
64+
1 == null // OK by eqIntNum
65+
66+
5967
class Fruit
6068

6169
implicit def eqFruit: Eq[Fruit, Fruit] = Eq
@@ -105,5 +113,22 @@ object equality {
105113
"abc" == bi // error
106114
bi == "abc" // error
107115
"world" == ps // error
116+
117+
val s1 = Set(1, 2, 3)
118+
val s2 = Set()
119+
120+
Nil == s1 // error
121+
s1 == Nil // error
122+
Nil == s2 // error
123+
s2 == Nil // error
124+
125+
import collection.parallel._
126+
val p1 = ParSeq(1, 2, 3)
127+
val p2 = ParSeq()
128+
Nil == p1 // OK
129+
p1 == Nil // OK
130+
Nil == p2 // OK
131+
p2 == Nil // Ok
132+
108133
}
109134
}

tests/neg/equality1.scala

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import dotty.DottyPredef.{eqAny => _, _}
2-
1+
import language.strictEquality
32
object equality1 {
43
class A
54
class B

0 commit comments

Comments
 (0)