Skip to content

Commit

Permalink
Reorganize Eq checking
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
odersky committed Jun 22, 2017
1 parent 9ca4c16 commit adec842
Show file tree
Hide file tree
Showing 8 changed files with 100 additions and 68 deletions.
3 changes: 2 additions & 1 deletion compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,6 @@ class Definitions {

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

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

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

def Eq_eqAny(implicit ctx: Context) = EqModule.requiredMethod(nme.eqAny)

lazy val XMLTopScopeModuleRef = ctx.requiredModuleRef("scala.xml.TopScope")

// Annotation base classes
Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/core/StdNames.scala
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,7 @@ object StdNames {
val staticClass : N = "staticClass"
val staticModule : N = "staticModule"
val staticPackage : N = "staticPackage"
val strictEquality: N = "strictEquality"
val synchronized_ : N = "synchronized"
val tag: N = "tag"
val tail: N = "tail"
Expand Down
110 changes: 57 additions & 53 deletions compiler/src/dotty/tools/dotc/typer/Implicits.scala
Original file line number Diff line number Diff line change
Expand Up @@ -545,29 +545,49 @@ trait Implicits { self: Typer =>
/** If `formal` is of the form ClassTag[T], where `T` is a class type,
* synthesize a class tag for `T`.
*/
def synthesizedClassTag(formal: Type, pos: Position)(implicit ctx: Context): Tree = {
if (formal.isRef(defn.ClassTagClass))
formal.argTypes match {
case arg :: Nil =>
fullyDefinedType(arg, "ClassTag argument", pos) match {
case defn.ArrayOf(elemTp) =>
val etag = inferImplicitArg(defn.ClassTagType.appliedTo(elemTp), error, pos)
if (etag.isEmpty) etag else etag.select(nme.wrap)
case tp if hasStableErasure(tp) =>
if (defn.isBottomClass(tp.typeSymbol))
error(where => i"attempt to take ClassTag of undetermined type for $where")
ref(defn.ClassTagModule)
.select(nme.apply)
.appliedToType(tp)
.appliedTo(clsOf(erasure(tp)))
.withPos(pos)
case tp =>
EmptyTree
}
case _ =>
EmptyTree
}
else EmptyTree
def synthesizedClassTag(formal: Type)(implicit ctx: Context): Tree =
formal.argTypes match {
case arg :: Nil =>
fullyDefinedType(arg, "ClassTag argument", pos) match {
case defn.ArrayOf(elemTp) =>
val etag = inferImplicitArg(defn.ClassTagType.appliedTo(elemTp), error, pos)
if (etag.isEmpty) etag else etag.select(nme.wrap)
case tp if hasStableErasure(tp) =>
if (defn.isBottomClass(tp.typeSymbol))
error(where => i"attempt to take ClassTag of undetermined type for $where")
ref(defn.ClassTagModule)
.select(nme.apply)
.appliedToType(tp)
.appliedTo(clsOf(erasure(tp)))
.withPos(pos)
case tp =>
EmptyTree
}
case _ =>
EmptyTree
}

/** If `formal` is of the form Eq[T, U], where no `Eq` instance exists for
* either `T` or `U`, synthesize `Eq.eqAny[T, U]` as solution.
*/
def synthesizedEq(formal: Type)(implicit ctx: Context): Tree = {
//println(i"synth eq $formal / ${formal.argTypes}%, %")
formal.argTypes match {
case args @ (arg1 :: arg2 :: Nil)
if !ctx.featureEnabled(defn.LanguageModuleClass, nme.strictEquality) &&
validEqAnyArgs(arg1, arg2)(ctx.fresh.setExploreTyperState) =>
ref(defn.Eq_eqAny).appliedToTypes(args).withPos(pos)
case _ =>
EmptyTree
}
}

def hasEq(tp: Type): Boolean =
inferImplicit(defn.EqType.appliedTo(tp, tp), EmptyTree, pos).isInstanceOf[SearchSuccess]

def validEqAnyArgs(tp1: Type, tp2: Type)(implicit ctx: Context) = {
List(tp1, tp2).foreach(fullyDefinedType(_, "eqAny argument", pos))
assumedCanEqual(tp1, tp2) || !hasEq(tp1) && !hasEq(tp2)
}

/** The context to be used when resolving a by-name implicit argument.
Expand Down Expand Up @@ -606,7 +626,13 @@ trait Implicits { self: Typer =>
error(where => s"ambiguous implicits: ${ambi.explanation} of $where")
EmptyTree
case failure: SearchFailure =>
val arg = synthesizedClassTag(formalValue, pos)
val arg =
if (formalValue.isRef(defn.ClassTagClass))
synthesizedClassTag(formalValue)
else if (formalValue.isRef(defn.EqClass))
synthesizedEq(formalValue)
else
EmptyTree
if (!arg.isEmpty) arg
else {
var msgFn = (where: String) =>
Expand Down Expand Up @@ -638,10 +664,10 @@ trait Implicits { self: Typer =>
}

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

private def isCoherent = pt.isRef(defn.EqClass)

assert(argument.isEmpty || argument.tpe.isValueType || argument.tpe.isInstanceOf[ExprType],
em"found: $argument: ${argument.tpe}, expected: $pt")

Expand Down Expand Up @@ -761,40 +789,16 @@ trait Implicits { self: Typer =>
case _ => false
}
}
// Does there exist an implicit value of type `Eq[tp, tp]`
// which is different from `eqAny`?
def hasEq(tp: Type): Boolean = {
def search(contextual: Boolean): Boolean =
new ImplicitSearch(defn.EqType.appliedTo(tp, tp), EmptyTree, pos)
.bestImplicit(contextual) match {
case result: SearchSuccess =>
result.ref.symbol != defn.Predef_eqAny ||
contextual && search(contextual = false)
case result: AmbiguousImplicits => true
case _ => false
}
search(contextual = true)
}

def validEqAnyArgs(tp1: Type, tp2: Type) = {
List(tp1, tp2).foreach(fullyDefinedType(_, "eqAny argument", pos))
assumedCanEqual(tp1, tp2) || !hasEq(tp1) && !hasEq(tp2) ||
{ implicits.println(i"invalid eqAny[$tp1, $tp2]"); false }
}
if (ctx.reporter.hasErrors)
nonMatchingImplicit(ref, ctx.reporter.removeBufferedMessages)
else if (contextual && !ctx.mode.is(Mode.ImplicitShadowing) &&
!shadowing.tpe.isError && !refSameAs(shadowing)) {
implicits.println(i"SHADOWING $ref in ${ref.termSymbol.owner} is shadowed by $shadowing in ${shadowing.symbol.owner}")
shadowedImplicit(ref, methPart(shadowing).tpe)
}
else generated1 match {
case TypeApply(fn, targs @ (arg1 :: arg2 :: Nil))
if fn.symbol == defn.Predef_eqAny && !validEqAnyArgs(arg1.tpe, arg2.tpe) =>
nonMatchingImplicit(ref, Nil)
case _ =>
SearchSuccess(generated1, ref, cand.level, ctx.typerState)
}
else
SearchSuccess(generated1, ref, cand.level, ctx.typerState)
}}

/** Given a list of implicit references, produce a list of all implicit search successes,
Expand All @@ -812,7 +816,7 @@ trait Implicits { self: Typer =>
case fail: SearchFailure =>
rankImplicits(pending1, acc)
case best: SearchSuccess =>
if (ctx.mode.is(Mode.ImplicitExploration)) best :: Nil
if (ctx.mode.is(Mode.ImplicitExploration) || isCoherent) best :: Nil
else {
val newPending = pending1.filter(cand1 =>
isAsGood(cand1.ref, best.ref, cand1.level, best.level)(nestedContext.setExploreTyperState))
Expand Down
11 changes: 0 additions & 11 deletions library/src/dotty/DottyPredef.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,9 @@ package dotty

import scala.reflect.ClassTag
import scala.Predef.???
import scala.collection.Seq

/** unimplemented implicit for TypeTag */
object DottyPredef {

/** A fall-back implicit to compare values of any types.
* The compiler will restrict implicit instances of `eqAny`. An instance
* `eqAny[T, U]` is _valid_ if `T <: U` or `U <: T` or both `T` and `U` are
* Eq-free. A type `S` is Eq-free if there is no implicit instance of `Eq[S, S]`.
* An implicit search will fail instead of returning an invalid `eqAny` instance.
* The method is here instead of the `Eq` object so that it can be disabled.
*/
implicit def eqAny[L, R]: Eq[L, R] = Eq

/** A class for implicit values that can serve as implicit conversions
* The implicit resolution algorithm will act as if there existed
* the additional implicit definition:
Expand Down
12 changes: 11 additions & 1 deletion library/src/scala/Eq.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package scala

import annotation.implicitNotFound
import scala.collection.{GenSeq, Set}

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

/** A fall-back "implicit" to compare values of any types.
* Even though this method is not declared implicit, the compiler will
* compute instances as solutions to `Eq[T, U]` queries if `T <: U` or `U <: T`
* or both `T` and `U` are Eq-free. A type `S` is Eq-free if there is no
* implicit instance of type `Eq[S, S]`.
*/
def eqAny[L, R]: Eq[L, R] = Eq

// Instances of `Eq` for common types

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

implicit def eqSeq[T, U](implicit eq: Eq[T, U]): Eq[Seq[T], Seq[U]] = Eq
implicit def eqSeq[T, U](implicit eq: Eq[T, U]): Eq[GenSeq[T], GenSeq[U]] = Eq
implicit def eqSet[T, U](implicit eq: Eq[T, U]): Eq[Set[T], Set[U]] = Eq

implicit def eqByteNum : Eq[Byte, Number] = Eq
implicit def eqNumByte : Eq[Number, Byte] = Eq
Expand Down
3 changes: 3 additions & 0 deletions library/src/scalaShadowing/language.scala
Original file line number Diff line number Diff line change
Expand Up @@ -195,4 +195,7 @@ object language {

/** Where imported, auto-tupling is disabled */
object noAutoTupling

/* Where imported loose equality using eqAny is disabled */
object strictEquality
}
25 changes: 25 additions & 0 deletions tests/neg/equality.scala
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ object equality {
null == Str("x")
null == null

1 == true // error

null == true // OK by eqProxy
true == null // error
null == 1 // OK by eqProxy or eqNumInt
1 == null // OK by eqIntNum


class Fruit

implicit def eqFruit: Eq[Fruit, Fruit] = Eq
Expand Down Expand Up @@ -105,5 +113,22 @@ object equality {
"abc" == bi // error
bi == "abc" // error
"world" == ps // error

val s1 = Set(1, 2, 3)
val s2 = Set()

Nil == s1 // error
s1 == Nil // error
Nil == s2 // error
s2 == Nil // error

import collection.parallel._
val p1 = ParSeq(1, 2, 3)
val p2 = ParSeq()
Nil == p1 // OK
p1 == Nil // OK
Nil == p2 // OK
p2 == Nil // Ok

}
}
3 changes: 1 addition & 2 deletions tests/neg/equality1.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import dotty.DottyPredef.{eqAny => _, _}

import language.strictEquality
object equality1 {
class A
class B
Expand Down

0 comments on commit adec842

Please sign in to comment.