From 309c5dee3e98c0bd21eec476148a4a72e01a4647 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Fri, 11 Dec 2020 15:18:09 +0100 Subject: [PATCH 1/9] Add Matchable trait History of squashed individual commits: Fix scala3doc Hierarchy test and CompilationTest Also, drop code that's no longer needed since we made Matchable an empty trait Re-enable tasty-interpreter test Since no more casts are inserted, this means tasty-interpreter runs as before. Make Matchable an empty marker trait Revert getClass changes Move getClass back to Any. This is needed to make zio pass. Make Matchable a trait Rename Scrutable => Matchable Re-enable utest zio is still broken with failures at runtime. Check nesteded scrutinees for scrutability Disable zio Move isInstanceOf to Scrutable Avoid reloading `AnyVal` Widen before checking types in assumedCanEqual Require scrutinees to be Scrutable Add test Rename Classable -> Scrutable Move getClass to Classable Avoid duplication in TreeInfo and Inliner Disable utest Trial: Towards a parametric top type --- .../src/dotty/tools/dotc/ast/TreeInfo.scala | 26 ++++++++++++------- .../dotty/tools/dotc/core/Definitions.scala | 25 +++++++++++++----- .../tools/dotc/core/OrderingConstraint.scala | 2 +- .../src/dotty/tools/dotc/core/StdNames.scala | 1 + .../tools/dotc/core/SymDenotations.scala | 4 +-- .../src/dotty/tools/dotc/core/Types.scala | 22 +++++++++++++--- .../dotty/tools/dotc/reporting/messages.scala | 5 +--- .../dotty/tools/dotc/transform/Erasure.scala | 9 ++++--- .../dotty/tools/dotc/typer/Applications.scala | 1 + .../src/dotty/tools/dotc/typer/Checking.scala | 6 +++++ .../dotty/tools/dotc/typer/Implicits.scala | 2 ++ .../src/dotty/tools/dotc/typer/Inliner.scala | 7 +---- .../src/dotty/tools/dotc/typer/ReTyper.scala | 2 +- .../dotty/tools/dotc/typer/Synthesizer.scala | 2 +- .../src/dotty/tools/dotc/typer/Typer.scala | 15 +++++++++-- .../src/dotty/tools/repl/ReplDriver.scala | 4 +-- .../quoted/runtime/impl/QuotesImpl.scala | 1 + .../BootstrappedOnlyCompilationTests.scala | 3 ++- .../dotty/tools/dotc/CompilationTests.scala | 2 ++ .../dotty/dokka/diagram/HierarchyTest.scala | 24 ++++++++++------- tests/new/test.scala | 10 ++----- tests/pos/i1044.scala | 3 ++- tests/run-macros/i8514.check | 8 +++--- tests/run-macros/i8514b.check | 1 + 24 files changed, 118 insertions(+), 67 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala index e42b635c5be6..f29bde70744d 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala @@ -397,17 +397,14 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] => case TypeApply(fn, _) => if (fn.symbol.is(Erased) || fn.symbol == defn.QuotedTypeModule_of || fn.symbol == defn.Predef_classOf) Pure else exprPurity(fn) case Apply(fn, args) => - def isKnownPureOp(sym: Symbol) = - sym.owner.isPrimitiveValueClass - || sym.owner == defn.StringClass - || defn.pureMethods.contains(sym) - if (tree.tpe.isInstanceOf[ConstantType] && isKnownPureOp(tree.symbol) // A constant expression with pure arguments is pure. - || (fn.symbol.isStableMember && !fn.symbol.is(Lazy))) // constructors of no-inits classes are stable + if isPureApply(tree, fn) then minOf(exprPurity(fn), args.map(exprPurity)) `min` Pure - else if (fn.symbol.is(Erased)) Pure - else if (fn.symbol.isStableMember) /* && fn.symbol.is(Lazy) */ + else if fn.symbol.is(Erased) then + Pure + else if fn.symbol.isStableMember /* && fn.symbol.is(Lazy) */ then minOf(exprPurity(fn), args.map(exprPurity)) `min` Idempotent - else Impure + else + Impure case Typed(expr, _) => exprPurity(expr) case Block(stats, expr) => @@ -440,6 +437,17 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] => def isPureBinding(tree: Tree)(using Context): Boolean = statPurity(tree) >= Pure + /** Is the application `tree` with function part `fn` known to be pure? + * Function value and arguments can still be impure. + */ + def isPureApply(tree: Tree, fn: Tree)(using Context): Boolean = + def isKnownPureOp(sym: Symbol) = + sym.owner.isPrimitiveValueClass + || sym.owner == defn.StringClass + || defn.pureMethods.contains(sym) + tree.tpe.isInstanceOf[ConstantType] && isKnownPureOp(tree.symbol) // A constant expression with pure arguments is pure. + || fn.symbol.isStableMember && !fn.symbol.is(Lazy) // constructors of no-inits classes are stable + /** The purity level of this reference. * @return * PurePath if reference is (nonlazy and stable) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index dfc0aa526004..24f151a7cfbe 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -259,10 +259,19 @@ class Definitions { */ @tu lazy val AnyClass: ClassSymbol = completeClass(enterCompleteClassSymbol(ScalaPackageClass, tpnme.Any, Abstract, Nil), ensureCtor = false) def AnyType: TypeRef = AnyClass.typeRef - @tu lazy val AnyValClass: ClassSymbol = completeClass(enterCompleteClassSymbol(ScalaPackageClass, tpnme.AnyVal, Abstract, List(AnyClass.typeRef))) + @tu lazy val MatchableClass: ClassSymbol = completeClass(enterCompleteClassSymbol(ScalaPackageClass, tpnme.Matchable, Trait, AnyType :: Nil), ensureCtor = false) + def MatchableType: TypeRef = MatchableClass.typeRef + @tu lazy val AnyValClass: ClassSymbol = + val res = completeClass(enterCompleteClassSymbol(ScalaPackageClass, tpnme.AnyVal, Abstract, List(AnyType, MatchableType))) + // Mark companion as absent, so that class does not get re-completed + val companion = ScalaPackageVal.info.decl(nme.AnyVal).symbol + companion.moduleClass.markAbsent() + companion.markAbsent() + res + def AnyValType: TypeRef = AnyValClass.typeRef - @tu lazy val Any_== : TermSymbol = enterMethod(AnyClass, nme.EQ, methOfAny(BooleanType), Final) + @tu lazy val Any_== : TermSymbol = enterMethod(AnyClass, nme.EQ, methOfAny(BooleanType), Final) @tu lazy val Any_!= : TermSymbol = enterMethod(AnyClass, nme.NE, methOfAny(BooleanType), Final) @tu lazy val Any_equals: TermSymbol = enterMethod(AnyClass, nme.equals_, methOfAny(BooleanType)) @tu lazy val Any_hashCode: TermSymbol = enterMethod(AnyClass, nme.hashCode_, MethodType(Nil, IntType)) @@ -288,7 +297,7 @@ class Definitions { @tu lazy val ObjectClass: ClassSymbol = { val cls = requiredClass("java.lang.Object") assert(!cls.isCompleted, "race for completing java.lang.Object") - cls.info = ClassInfo(cls.owner.thisType, cls, AnyClass.typeRef :: Nil, newScope) + cls.info = ClassInfo(cls.owner.thisType, cls, List(AnyType, MatchableType), newScope) cls.setFlag(NoInits | JavaDefined) // The companion object doesn't really exist, so it needs to be marked as @@ -444,7 +453,7 @@ class Definitions { MethodType(List(ThrowableType), NothingType)) @tu lazy val NothingClass: ClassSymbol = enterCompleteClassSymbol( - ScalaPackageClass, tpnme.Nothing, AbstractFinal, List(AnyClass.typeRef)) + ScalaPackageClass, tpnme.Nothing, AbstractFinal, List(AnyType)) def NothingType: TypeRef = NothingClass.typeRef @tu lazy val NullClass: ClassSymbol = { val parent = if (ctx.explicitNulls) AnyType else ObjectType @@ -520,7 +529,7 @@ class Definitions { // but does not define it as an explicit class. enterCompleteClassSymbol( ScalaPackageClass, tpnme.Singleton, PureInterfaceCreationFlags | Final, - List(AnyClass.typeRef), EmptyScope) + List(AnyType), EmptyScope) @tu lazy val SingletonType: TypeRef = SingletonClass.typeRef @tu lazy val CollectionSeqType: TypeRef = requiredClassRef("scala.collection.Seq") @@ -1144,6 +1153,8 @@ class Definitions { // ----- Symbol sets --------------------------------------------------- + @tu lazy val topClasses: Set[Symbol] = Set(AnyClass, MatchableClass, ObjectClass, AnyValClass) + @tu lazy val AbstractFunctionType: Array[TypeRef] = mkArityArray("scala.runtime.AbstractFunction", MaxImplementedFunctionArity, 0) val AbstractFunctionClassPerRun: PerRun[Array[Symbol]] = new PerRun(AbstractFunctionType.map(_.symbol.asClass)) def AbstractFunctionClass(n: Int)(using Context): Symbol = AbstractFunctionClassPerRun()(using ctx)(n) @@ -1372,7 +1383,7 @@ class Definitions { @tu lazy val ShadowableImportNames: Set[TermName] = Set("Predef".toTermName) /** Class symbols for which no class exist at runtime */ - @tu lazy val NotRuntimeClasses: Set[Symbol] = Set(AnyClass, AnyValClass, NullClass, NothingClass) + @tu lazy val NotRuntimeClasses: Set[Symbol] = Set(AnyClass, MatchableClass, AnyValClass, NullClass, NothingClass) @tu lazy val SpecialClassTagClasses: Set[Symbol] = Set(UnitClass, AnyClass, AnyValClass) @@ -1672,6 +1683,7 @@ class Definitions { @tu lazy val specialErasure: SimpleIdentityMap[Symbol, ClassSymbol] = SimpleIdentityMap.empty[Symbol] .updated(AnyClass, ObjectClass) + .updated(MatchableClass, ObjectClass) .updated(AnyValClass, ObjectClass) .updated(SingletonClass, ObjectClass) .updated(TupleClass, ProductClass) @@ -1683,6 +1695,7 @@ class Definitions { @tu lazy val syntheticScalaClasses: List[TypeSymbol] = { val synth = List( AnyClass, + MatchableClass, AnyRefAlias, AnyKindClass, andType, diff --git a/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala b/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala index 08702827a9dd..01cadbc096dc 100644 --- a/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala +++ b/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala @@ -297,7 +297,7 @@ class OrderingConstraint(private val boundsMap: ParamBounds, /** If `inst` is a TypeBounds, make sure it does not contain toplevel * references to `param` (see `Constraint#occursAtToplevel` for a definition * of "toplevel"). - * Any such references are replace by `Nothing` in the lower bound and `Any` + * Any such references are replaced by `Nothing` in the lower bound and `Any` * in the upper bound. * References can be direct or indirect through instantiations of other * parameters in the constraint. diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index ac509caf9a12..9b51c4ea4d08 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -387,6 +387,7 @@ object StdNames { val Ref: N = "Ref" val RootPackage: N = "RootPackage" val RootClass: N = "RootClass" + val Matchable: N = "Matchable" val Select: N = "Select" val Shape: N = "Shape" val StringContext: N = "StringContext" diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index ce1671e0549a..a6cd52b7538b 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -2246,9 +2246,7 @@ object SymDenotations { if (pcls.isCompleting) recur(pobjs1, acc) else val pobjMembers = pcls.nonPrivateMembersNamed(name).filterWithPredicate { d => - // Drop members of `Any` and `Object` - val owner = d.symbol.maybeOwner - (owner ne defn.AnyClass) && (owner ne defn.ObjectClass) + !defn.topClasses.contains(d.symbol.maybeOwner) // Drop members of top classes } recur(pobjs1, acc.union(pobjMembers)) case nil => diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index d8efa3ca8973..87d599924c2f 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -209,6 +209,10 @@ object Types { def isAnyRef(using Context): Boolean = isRef(defn.ObjectClass, skipRefined = false) def isAnyKind(using Context): Boolean = isRef(defn.AnyKindClass, skipRefined = false) + def isTopType(using Context): Boolean = dealias match + case tp: TypeRef => defn.topClasses.contains(tp.symbol) + case _ => false + /** Is this type exactly Nothing (no vars, aliases, refinements etc allowed)? */ def isExactlyNothing(using Context): Boolean = this match { case tp: TypeRef => @@ -512,6 +516,20 @@ object Types { case _ => false + /** Same as hasClassSmbol(MatchableClass), except that we also follow the constraint + * bounds of type variables in the constraint. + */ + def isMatchableBound(using Context): Boolean = dealias match + case tp: TypeRef => tp.symbol == defn.MatchableClass + case tp: TypeParamRef => + ctx.typerState.constraint.entry(tp) match + case bounds: TypeBounds => bounds.hi.isMatchableBound + case _ => false + case tp: TypeProxy => tp.underlying.isMatchableBound + case tp: AndType => tp.tp1.isMatchableBound || tp.tp2.isMatchableBound + case tp: OrType => tp.tp1.isMatchableBound && tp.tp2.isMatchableBound + case _ => false + /** The term symbol associated with the type */ @tailrec final def termSymbol(using Context): Symbol = this match { case tp: TermRef => tp.symbol @@ -3812,10 +3830,6 @@ object Types { def unapply(tl: PolyType): Some[(List[LambdaParam], Type)] = Some((tl.typeParams, tl.resType)) - - def any(n: Int)(using Context): PolyType = - apply(syntheticParamNames(n))( - pt => List.fill(n)(TypeBounds.empty), pt => defn.AnyType) } private object DepStatus { diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index 64cb29ac999b..19571e92f297 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -266,10 +266,7 @@ import transform.SymUtils._ val postScript = addenda.find(!_.isEmpty) match case Some(p) => p case None => - if expected.isAny - || expected.isAnyRef - || expected.isRef(defn.AnyValClass) - || found.isBottomType + if expected.isTopType || found.isBottomType then "" else ctx.typer.importSuggestionAddendum(ViewProto(found.widen, expected)) val (where, printCtx) = Formatting.disambiguateTypes(found2, expected2) diff --git a/compiler/src/dotty/tools/dotc/transform/Erasure.scala b/compiler/src/dotty/tools/dotc/transform/Erasure.scala index 42a63e53fc3c..e66e4f5d15e2 100644 --- a/compiler/src/dotty/tools/dotc/transform/Erasure.scala +++ b/compiler/src/dotty/tools/dotc/transform/Erasure.scala @@ -61,8 +61,8 @@ class Erasure extends Phase with DenotTransformer { // After erasure, all former Any members are now Object members val ClassInfo(pre, _, ps, decls, selfInfo) = ref.info val extendedScope = decls.cloneScope - for (decl <- defn.AnyClass.classInfo.decls) - if (!decl.isConstructor) extendedScope.enter(decl) + for decl <- defn.AnyClass.classInfo.decls do + if !decl.isConstructor then extendedScope.enter(decl) ref.copySymDenotation( info = transformInfo(ref.symbol, ClassInfo(pre, defn.ObjectClass, ps, extendedScope, selfInfo)) @@ -71,11 +71,12 @@ class Erasure extends Phase with DenotTransformer { else { val oldSymbol = ref.symbol val newSymbol = - if ((oldSymbol.owner eq defn.AnyClass) && oldSymbol.isConstructor) + if ((oldSymbol.owner eq defn.AnyClass) && oldSymbol.isConstructor) then + //assert(false) defn.ObjectClass.primaryConstructor else oldSymbol val oldOwner = ref.owner - val newOwner = if (oldOwner eq defn.AnyClass) defn.ObjectClass else oldOwner + val newOwner = if oldOwner == defn.AnyClass then defn.ObjectClass else oldOwner val oldName = ref.name val newName = ref.targetName val oldInfo = ref.info diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 1e9b013497cb..f461a493d661 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -1158,6 +1158,7 @@ trait Applications extends Compatibility { def typedUnApply(tree: untpd.Apply, selType: Type)(using Context): Tree = { record("typedUnApply") val Apply(qual, args) = tree + checkMatchable(selType, tree.srcPos, pattern = true) def notAnExtractor(tree: Tree): Tree = // prefer inner errors diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index b31c14e60eee..43c38b1468b6 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -1246,6 +1246,11 @@ trait Checking { if preExisting.exists || seen.contains(tname) then report.error(em"@targetName annotation ${'"'}$tname${'"'} clashes with other definition in same scope", stat.srcPos) if stat.isDef then seen += tname + + def checkMatchable(tp: Type, pos: SrcPos, pattern: Boolean)(using Context): Unit = + if !tp.derivesFrom(defn.MatchableClass) && sourceVersion.isAtLeast(`3.1-migration`) then + val kind = if pattern then "pattern selector " else "" + report.warning(em"${kind}type $tp should not be scrutinized", pos) } trait ReChecking extends Checking { @@ -1256,6 +1261,7 @@ trait ReChecking extends Checking { override def checkFullyAppliedType(tree: Tree)(using Context): Unit = () override def checkEnumCaseRefsLegal(cdef: TypeDef, enumCtx: Context)(using Context): Unit = () override def checkAnnotApplicable(annot: Tree, sym: Symbol)(using Context): Boolean = true + override def checkMatchable(tp: Type, pos: SrcPos, pattern: Boolean)(using Context): Unit = () } trait NoChecking extends ReChecking { diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index 61eb32bb11f5..81c40a5674e1 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -898,6 +898,8 @@ trait Implicits: case TypeBounds(lo, hi) if lo.ne(hi) && !t.symbol.is(Opaque) => apply(hi) case _ => t } + case t: SingletonType => + apply(t.widen) case t: RefinedType => apply(t.parent) case _ => diff --git a/compiler/src/dotty/tools/dotc/typer/Inliner.scala b/compiler/src/dotty/tools/dotc/typer/Inliner.scala index 83b1b158836c..6b062b5833a0 100644 --- a/compiler/src/dotty/tools/dotc/typer/Inliner.scala +++ b/compiler/src/dotty/tools/dotc/typer/Inliner.scala @@ -545,10 +545,6 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(using Context) { case TypeApply(fn, _) => if (fn.symbol.is(Erased) || fn.symbol == defn.QuotedTypeModule_of) true else apply(fn) case Apply(fn, args) => - def isKnownPureOp(sym: Symbol) = - sym.owner.isPrimitiveValueClass - || sym.owner == defn.StringClass - || defn.pureMethods.contains(sym) val isCaseClassApply = { val cls = tree.tpe.classSymbol val meth = fn.symbol @@ -557,8 +553,7 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(using Context) { meth.owner.linkedClass.is(Case) && cls.isNoInitsRealClass } - if (tree.tpe.isInstanceOf[ConstantType] && isKnownPureOp(tree.symbol) // A constant expression with pure arguments is pure. - || (fn.symbol.isStableMember && !fn.symbol.is(Lazy))) // constructors of no-inits classes are stable + if isPureApply(tree, fn) then apply(fn) && args.forall(apply) else if (isCaseClassApply) args.forall(apply) diff --git a/compiler/src/dotty/tools/dotc/typer/ReTyper.scala b/compiler/src/dotty/tools/dotc/typer/ReTyper.scala index f119a9b82019..91b7f3873a9f 100644 --- a/compiler/src/dotty/tools/dotc/typer/ReTyper.scala +++ b/compiler/src/dotty/tools/dotc/typer/ReTyper.scala @@ -141,4 +141,4 @@ class ReTyper extends Typer with ReChecking { override protected def addAccessorDefs(cls: Symbol, body: List[Tree])(using Context): List[Tree] = body override protected def checkEqualityEvidence(tree: tpd.Tree, pt: Type)(using Context): Unit = () override protected def matchingApply(methType: MethodOrPoly, pt: FunProto)(using Context): Boolean = true -} +} \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala index bcb9f0eac43a..09eb36b5c348 100644 --- a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala @@ -62,7 +62,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): val arg :: Nil = args val t = arg.tpe & tp2 If( - arg.select(defn.Any_isInstanceOf).appliedToType(tp2), + arg.isInstance(tp2), ref(defn.SomeClass.companionModule.termRef).select(nme.apply) .appliedToType(t) .appliedTo(arg.select(nme.asInstanceOf_).appliedToType(t)), diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index db87d2f45faa..7a6074f48020 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -682,7 +682,10 @@ class Typer extends Namer case templ: untpd.Template => import untpd._ var templ1 = templ - def isEligible(tp: Type) = tp.exists && !tp.typeSymbol.is(Final) && !tp.isRef(defn.AnyClass) + def isEligible(tp: Type) = + tp.exists + && !tp.typeSymbol.is(Final) + && (!tp.isTopType || tp.isAnyRef) // Object is the only toplevel class that can be instantiated if (templ1.parents.isEmpty && isFullyDefined(pt, ForceDegree.flipBottom) && isSkolemFree(pt) && @@ -769,6 +772,7 @@ class Typer extends Namer def handlePattern: Tree = { val tpt1 = typedTpt if (!ctx.isAfterTyper && pt != defn.ImplicitScrutineeTypeRef) + checkMatchable(pt, tree.srcPos, pattern = true) withMode(Mode.GadtConstraintInference) { TypeComparer.constrainPatternType(tpt1.tpe, pt) } @@ -1320,7 +1324,7 @@ class Typer extends Namer typed(desugar.makeCaseLambda(tree.cases, checkMode, protoFormals.length).withSpan(tree.span), pt) } case _ => - if (tree.isInline) checkInInlineContext("inline match", tree.srcPos) + if tree.isInline then checkInInlineContext("inline match", tree.srcPos) val sel1 = typedExpr(tree.selector) val selType = fullyDefinedType(sel1.tpe, "pattern selector", tree.span).widen @@ -3527,6 +3531,13 @@ class Typer extends Namer case _ => } + // try an Any -> Matchable conversion + if pt.isMatchableBound && !wtp.derivesFrom(defn.MatchableClass) then + checkMatchable(wtp, tree.srcPos, pattern = false) + val target = AndType(tree.tpe.widenExpr, defn.MatchableType) + if target <:< pt then + return readapt(tree.cast(target)) + // try an implicit conversion val prevConstraint = ctx.typerState.constraint def recover(failure: SearchFailureType) = diff --git a/compiler/src/dotty/tools/repl/ReplDriver.scala b/compiler/src/dotty/tools/repl/ReplDriver.scala index dc570d3c0c4e..966eb2107fb5 100644 --- a/compiler/src/dotty/tools/repl/ReplDriver.scala +++ b/compiler/src/dotty/tools/repl/ReplDriver.scala @@ -291,9 +291,7 @@ class ReplDriver(settings: Array[String], info.bounds.hi.finalResultType .membersBasedOnFlags(required = Method, excluded = Accessor | ParamAccessor | Synthetic | Private) .filterNot { denot => - denot.symbol.owner == defn.AnyClass || - denot.symbol.owner == defn.ObjectClass || - denot.symbol.isConstructor + defn.topClasses.contains(denot.symbol.owner) || denot.symbol.isConstructor } val vals = diff --git a/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala b/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala index e6c108003b87..5500bef5c96a 100644 --- a/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala +++ b/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala @@ -2534,6 +2534,7 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler def ScalaPackage: Symbol = dotc.core.Symbols.defn.ScalaPackageVal def ScalaPackageClass: Symbol = dotc.core.Symbols.defn.ScalaPackageClass def AnyClass: Symbol = dotc.core.Symbols.defn.AnyClass + def MatchableClass: Symbol = dotc.core.Symbols.defn.MatchableClass def AnyValClass: Symbol = dotc.core.Symbols.defn.AnyValClass def ObjectClass: Symbol = dotc.core.Symbols.defn.ObjectClass def AnyRefClass: Symbol = dotc.core.Symbols.defn.AnyRefAlias diff --git a/compiler/test/dotty/tools/dotc/BootstrappedOnlyCompilationTests.scala b/compiler/test/dotty/tools/dotc/BootstrappedOnlyCompilationTests.scala index 80dfa07fdfe9..633f2d6758d2 100644 --- a/compiler/test/dotty/tools/dotc/BootstrappedOnlyCompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/BootstrappedOnlyCompilationTests.scala @@ -137,8 +137,9 @@ class BootstrappedOnlyCompilationTests extends ParallelTesting { compileFilesInDir("tests/run-custom-args/tasty-inspector", withTastyInspectorOptions) ) val tests = - if (scala.util.Properties.isWin) basicTests + if scala.util.Properties.isWin then basicTests else compileDir("tests/run-custom-args/tasty-interpreter", withTastyInspectorOptions) :: basicTests + aggregateTests(tests: _*).checkRuns() } diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index 39f96f0fa5a5..0016bf987352 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -164,6 +164,8 @@ class CompilationTests { compileFile("tests/neg-custom-args/typeclass-derivation2.scala", defaultOptions.and("-Yerased-terms")), compileFile("tests/neg-custom-args/i5498-postfixOps.scala", defaultOptions withoutLanguageFeature "postfixOps"), compileFile("tests/neg-custom-args/deptypes.scala", defaultOptions.and("-language:experimental.dependent")), + compileFile("tests/neg-custom-args/matchable.scala", defaultOptions.and("-Xfatal-warnings", "-source", "3.1")), + compileFile("tests/neg-custom-args/i7314.scala", defaultOptions.and("-Xfatal-warnings", "-source", "3.1")) ).checkExpectedErrors() } diff --git a/scala3doc/test/dotty/dokka/diagram/HierarchyTest.scala b/scala3doc/test/dotty/dokka/diagram/HierarchyTest.scala index e2b166ddda19..ae1ba4b80127 100644 --- a/scala3doc/test/dotty/dokka/diagram/HierarchyTest.scala +++ b/scala3doc/test/dotty/dokka/diagram/HierarchyTest.scala @@ -12,13 +12,15 @@ class HierarchyTest extends ScaladocTest("hierarchy"): AfterDocumentablesTransformation { m => m.visitMembers { x => if (x.getName == "C1") { - assertEquals(List("A1", "A2[Int]", "A3[Int, String]", "Any", "B1", "B2", "B3", "Object"), x.getParentsAsStrings) + assertEquals(List("A1", "A2[Int]", "A3[Int, String]", "Any", "B1", "B2", "B3", "Matchable", "Object"), x.getParentsAsStrings) assertEquals(List("B1", "B2", "B3"), x.getDirectParentsAsStrings) assertEquals(List("E1", "E2"), x.getKnownChildrenAsStrings) - val graph = MemberExtension.getFrom(x).map(_.graph) + val graph = MemberExtension.getFrom(x).map(_.graph) assertTrue("Graph is empty!", graph.isDefined) assertEquals( Set( + "Object" -> "Matchable", + "Matchable" -> "Any", "Object" -> "Any", "A1" -> "Object", "A2[Int]" -> "Object", @@ -42,15 +44,15 @@ class HierarchyTest extends ScaladocTest("hierarchy"): ) } if (x.getName == "E2") { - assertEquals(List("A1", "A2[Int]", "A3[Int, String]", "Any", "B1", "B2", "B3", "C1[Int, Boolean, Any]", "D2[Int, Boolean]", "D3", "Object"), x.getParentsAsStrings) + assertEquals(List("A1", "A2[Int]", "A3[Int, String]", "Any", "B1", "B2", "B3", "C1[Int, Boolean, Any]", "D2[Int, Boolean]", "D3", "Matchable", "Object"), x.getParentsAsStrings) assertEquals(List("C1[Int, Boolean, Any]", "D2[Int, Boolean]", "D3"), x.getDirectParentsAsStrings) assertEquals(List.empty, x.getKnownChildrenAsStrings) - val graph = MemberExtension.getFrom(x).map(_.graph) + val graph = MemberExtension.getFrom(x).map(_.graph) assertTrue("Graph is empty!", graph.isDefined) assertEquals( Set( "Object" -> "Any", - // "A1" -> "Object", // These are not applicable beacuase of bug and its workaround + // "A1" -> "Object", // These are not applicable beacuase of bug and its workaround // "A2[Int]" -> "Object", // More info at ClassLikeSupport.scala:37 // "A3[Int, String]" -> "Object", // "B1" -> "Object", @@ -65,6 +67,8 @@ class HierarchyTest extends ScaladocTest("hierarchy"): // "C1[Int, Boolean, Any]" -> "B1", // "C1[Int, Boolean, Any]" -> "B2", // "C1[Int, Boolean, Any]" -> "B3", + "Object" -> "Matchable", + "Matchable" -> "Any", "E2" -> "D2[Int, Boolean]", "E2" -> "D3", "D2[Int, Boolean]" -> "Object", @@ -75,16 +79,18 @@ class HierarchyTest extends ScaladocTest("hierarchy"): ) } if (x.getName == "A2") { - assertEquals(List("Any", "Object"), x.getParentsAsStrings) + assertEquals(List("Any", "Matchable", "Object"), x.getParentsAsStrings) assertEquals(List.empty, x.getDirectParentsAsStrings) assertEquals(List("B2", "B3", "C1[A, B, C]", "E1", "E2"), x.getKnownChildrenAsStrings) - val graph = MemberExtension.getFrom(x).map(_.graph) + val graph = MemberExtension.getFrom(x).map(_.graph) assertTrue("Graph is empty!", graph.isDefined) assertEquals( Set( + "Object" -> "Matchable", + "Matchable" -> "Any", "Object" -> "Any", "A2[T]" -> "Object", - "B2" -> "A2[T]", // These are not actually true, becuase we lose information about hierarchy in subtypes and their possible mapping to supertypes other that that type itself, e. g. linking to `Object` + "B2" -> "A2[T]", // These are not actually true, becuase we lose information about hierarchy in subtypes and their possible mapping to supertypes other that that type itself, e. g. linking to `Object` "B3" -> "A2[T]", "C1[A, B, C]" -> "A2[T]", "E1" -> "A2[T]", @@ -95,4 +101,4 @@ class HierarchyTest extends ScaladocTest("hierarchy"): } } } - ) + ) diff --git a/tests/new/test.scala b/tests/new/test.scala index 6563875e2cac..ac8f67d1530f 100644 --- a/tests/new/test.scala +++ b/tests/new/test.scala @@ -1,9 +1,3 @@ +object Test: -trait SemiGroup[T] { - extension (x: T) def combine(y: T): T -} -trait Monoid[T] extends SemiGroup[T] { - def unit: T -} -def sum[T: Monoid](xs: List[T]): T = - xs.foldLeft(implicitly[Monoid[T]].unit)((x, y) => x.combine(y)) + def test = ??? diff --git a/tests/pos/i1044.scala b/tests/pos/i1044.scala index a984dbd67449..d5afdd01fac8 100644 --- a/tests/pos/i1044.scala +++ b/tests/pos/i1044.scala @@ -1,3 +1,4 @@ object Test { - val x = ???.getClass.getMethods.head.getParameterTypes.mkString(",") + def f[X](x: X) = + x.getClass.getMethods.head.getParameterTypes.mkString(",") } diff --git a/tests/run-macros/i8514.check b/tests/run-macros/i8514.check index f4acc82925cb..e38c2ff57675 100644 --- a/tests/run-macros/i8514.check +++ b/tests/run-macros/i8514.check @@ -1,4 +1,4 @@ -List(class Object, class Any) -List(class A, class Object, class Any) -List(class B, class A, class Object, class Any) -List(class C, class B, class A, class Object, class Any) +List(class Object, trait Matchable, class Any) +List(class A, class Object, trait Matchable, class Any) +List(class B, class A, class Object, trait Matchable, class Any) +List(class C, class B, class A, class Object, trait Matchable, class Any) diff --git a/tests/run-macros/i8514b.check b/tests/run-macros/i8514b.check index fcf27e623f06..7340243f9771 100644 --- a/tests/run-macros/i8514b.check +++ b/tests/run-macros/i8514b.check @@ -1,4 +1,5 @@ B A[[T >: scala.Nothing <: scala.Any] => P[T], scala.Predef.String] java.lang.Object +scala.Matchable scala.Any From 61153e71ceccbe8e35afc06096b185df5f0227da Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Fri, 11 Dec 2020 10:55:22 +0100 Subject: [PATCH 2/9] Address review comments --- .../src/dotty/tools/dotc/core/StdNames.scala | 2 +- .../src/dotty/tools/dotc/typer/ReTyper.scala | 2 +- library/src/scala/quoted/Quotes.scala | 3 +++ tests/neg/extend-matchable.scala | 23 +++++++++++++++++++ tests/pos/i1044.scala | 2 ++ 5 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 tests/neg/extend-matchable.scala diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 9b51c4ea4d08..2a4c6a500f07 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -371,6 +371,7 @@ object StdNames { val Import: N = "Import" val Literal: N = "Literal" val LiteralAnnotArg: N = "LiteralAnnotArg" + val Matchable: N = "Matchable" val MatchCase: N = "MatchCase" val MirroredElemTypes: N = "MirroredElemTypes" val MirroredElemLabels: N = "MirroredElemLabels" @@ -387,7 +388,6 @@ object StdNames { val Ref: N = "Ref" val RootPackage: N = "RootPackage" val RootClass: N = "RootClass" - val Matchable: N = "Matchable" val Select: N = "Select" val Shape: N = "Shape" val StringContext: N = "StringContext" diff --git a/compiler/src/dotty/tools/dotc/typer/ReTyper.scala b/compiler/src/dotty/tools/dotc/typer/ReTyper.scala index 91b7f3873a9f..f119a9b82019 100644 --- a/compiler/src/dotty/tools/dotc/typer/ReTyper.scala +++ b/compiler/src/dotty/tools/dotc/typer/ReTyper.scala @@ -141,4 +141,4 @@ class ReTyper extends Typer with ReChecking { override protected def addAccessorDefs(cls: Symbol, body: List[Tree])(using Context): List[Tree] = body override protected def checkEqualityEvidence(tree: tpd.Tree, pt: Type)(using Context): Unit = () override protected def matchingApply(methType: MethodOrPoly, pt: FunProto)(using Context): Boolean = true -} \ No newline at end of file +} diff --git a/library/src/scala/quoted/Quotes.scala b/library/src/scala/quoted/Quotes.scala index 7e0a29c75f02..7cf91961b2eb 100644 --- a/library/src/scala/quoted/Quotes.scala +++ b/library/src/scala/quoted/Quotes.scala @@ -3411,6 +3411,9 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching => /** The class symbol of core class `scala.Any`. */ def AnyClass: Symbol + /** The class symbol of core trait `scala.Matchable` */ + def MatchableClass: Symbol + /** The class symbol of core class `scala.AnyVal`. */ def AnyValClass: Symbol diff --git a/tests/neg/extend-matchable.scala b/tests/neg/extend-matchable.scala new file mode 100644 index 000000000000..18dee7a1fb53 --- /dev/null +++ b/tests/neg/extend-matchable.scala @@ -0,0 +1,23 @@ +trait M0 extends Matchable // inferred base type is AnyRef +trait M extends Any, Matchable + +class C extends Matchable // OK inferred base type is AnyRef +class D(x: Int) extends AnyVal, Matchable // OK +class E extends AnyRef, Matchable // OK +class F extends Any, Matchable // error: Any does not have a constructor + +class C1 extends M // OK inferred base type is AnyRef +class D1(x: Int) extends AnyVal, M // OK +class E1 extends AnyRef, M // OK +class F1 extends Any, M // error: Any does not have a constructor + +class C2 extends M0 // OK inferred base type is AnyRef +class D2(x: Int) extends AnyVal, M0 // error: illegal trait inheritance +class E2 extends AnyRef, M0 // OK +class F2 extends Any, M0 // error: Any does not have a constructor // error: illegal trait inheritance + + + + + + diff --git a/tests/pos/i1044.scala b/tests/pos/i1044.scala index d5afdd01fac8..bc1e500462c7 100644 --- a/tests/pos/i1044.scala +++ b/tests/pos/i1044.scala @@ -1,4 +1,6 @@ object Test { def f[X](x: X) = + ???.getClass.getMethods.head.getParameterTypes.mkString(",") + // this is the current behavior. But it would also be OK if `???.getClass` fails x.getClass.getMethods.head.getParameterTypes.mkString(",") } From 5186a48489b883abc16c74079ee94cfdaa47ad75 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Fri, 11 Dec 2020 14:29:51 +0100 Subject: [PATCH 3/9] Improve warning message and add doc page Also, add back tests --- .../src/dotty/tools/dotc/typer/Checking.scala | 6 +- .../reference/other-new-features/matchable.md | 68 +++++++++++++++++++ docs/sidebar.yml | 2 + tests/neg-custom-args/i7314.scala | 9 +++ tests/neg-custom-args/matchable.scala | 27 ++++++++ tests/run/matchable.check | 8 +++ tests/run/matchable.scala | 29 ++++++++ 7 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 docs/docs/reference/other-new-features/matchable.md create mode 100644 tests/neg-custom-args/i7314.scala create mode 100644 tests/neg-custom-args/matchable.scala create mode 100644 tests/run/matchable.check create mode 100644 tests/run/matchable.scala diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 43c38b1468b6..5b510fb57340 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -1249,8 +1249,10 @@ trait Checking { def checkMatchable(tp: Type, pos: SrcPos, pattern: Boolean)(using Context): Unit = if !tp.derivesFrom(defn.MatchableClass) && sourceVersion.isAtLeast(`3.1-migration`) then - val kind = if pattern then "pattern selector " else "" - report.warning(em"${kind}type $tp should not be scrutinized", pos) + val kind = if pattern then "pattern selector" else "value" + report.warning( + em"""${kind} should be an instance of Matchable, + |but it has unmatchable type $tp instead""", pos) } trait ReChecking extends Checking { diff --git a/docs/docs/reference/other-new-features/matchable.md b/docs/docs/reference/other-new-features/matchable.md new file mode 100644 index 000000000000..68f152dd2b4e --- /dev/null +++ b/docs/docs/reference/other-new-features/matchable.md @@ -0,0 +1,68 @@ +layout: doc-page +title: The Matchable Trait +--- + +A new trait `Matchable` controls the ability to pattern match. + +### The Problem + +The Scala 3 standard library has a type `IArray` for immutable +arrays that is defined like this: + +```scala + opaque type IArray[+T] = Array[_ <: T] +``` +The `IArray` type offers extension methods for `length` and `apply`, but not for `update`; hence it seems values of type `IArray` cannot be updated. + +However, there is a potential hole due to pattern matching. Consider: +```scala +val imm: IArray[Int] = ... +imm match + case a: Array[Int] => a(0) = 1 +``` +The test will succeed at runtime since `IArray`s _are_ represented as +`Array`s at runtime. But if we allowed it, it would break the fundamental abstraction of immutable arrays. + +__Aside:__ One could also achieve the same by casting: +```scala +imm.asInstanceOf[Array[Int]](0) = 1 +``` +But that is not as much of a problem since in Scala `asInstanceOf` is understood to be low-level and unsafe. By contrast, a pattern match that compiles without warning or error should not break abstractions. + +Note also that the problem is not tied to opaque types as match selectors. The following slight variant with a value of parametric +type `T` as match selector leads to the same problem: + +```scala +def f[T](x: T) = x match + case a: Array[Int] => a(0) = 0 +f(imm) +``` +Finally, note that the problem is not linked to just opaque types. No unbounded type parameter or abstract type should be decomposable with a pattern match. + +### The Solution + +There is a new type `scala.Matchable` that controls pattern matching. When typing a pattern match of a constructor pattern `C(...)` or +a type pattern `_: C` it is required that the selector type conforms +to `Matchable`. If that's not the case a warning is issued. For instance when compiling the example at the start of this section we get: +``` +> sc ../new/test.scala -source 3.1 +-- Warning: ../new/test.scala:4:12 --------------------------------------------- +4 | case a: Array[Int] => a(0) = 0 + | ^^^^^^^^^^ + | pattern selector should be an instance of Matchable, + | but it has unmatchable type IArray[Int] instead +``` +To allow migration from Scala 2 and cross-compiling +between Scala 2 and 3 the warning is turned on only for `-source 3.1-migration` or higher. + +`Matchable` is a universal trait with `Any` as its parent class. It is +extended by both `AnyVal` and `AnyRef`. Since `Matchable` is a supertype of every concrete value or reference class it means that instances of such classes can be matched as before. However, match selectors of the following types will produce a warning: + + - Type `Any`: if pattern matching is required one should use `Matchable` instead. + - Unbounded type parameters and abstract types: If pattern matching is required they should have an upper bound `Matchable`. + - Type parameters and abstract types that are only bounded by some + universal trait: Again, `Matchable` should be added as a bound. + +`Matchable` is currently a marker trait without any methods. Over time +we might migrate methods `getClass` and `isInstanceOf` to it, since these are closely related to pattern-matching. + diff --git a/docs/sidebar.yml b/docs/sidebar.yml index 371323a3e8e8..5fccddd313d6 100644 --- a/docs/sidebar.yml +++ b/docs/sidebar.yml @@ -105,6 +105,8 @@ sidebar: url: docs/reference/other-new-features/parameter-untupling.html - title: Kind Polymorphism url: docs/reference/other-new-features/kind-polymorphism.html + - title: Matchable Trait + url: docs/reference/other-new-features/matchable.html - title: threadUnsafe Annotation url: docs/reference/other-new-features/threadUnsafe-annotation.html - title: targetName Annotation diff --git a/tests/neg-custom-args/i7314.scala b/tests/neg-custom-args/i7314.scala new file mode 100644 index 000000000000..fbdf3dfc477f --- /dev/null +++ b/tests/neg-custom-args/i7314.scala @@ -0,0 +1,9 @@ +@main def Test = + // conversion out of the opaque type: + val imm1 = IArray(1,2,3) // supposedly immutable + println(imm1(0)) // 1 + imm1 match { + case a: Array[Int] => // error: should not be scrutinized + a(0) = 0 + } + println(imm1(0)) // 0 diff --git a/tests/neg-custom-args/matchable.scala b/tests/neg-custom-args/matchable.scala new file mode 100644 index 000000000000..388af29d25cf --- /dev/null +++ b/tests/neg-custom-args/matchable.scala @@ -0,0 +1,27 @@ +def foo[T](x: T): Matchable = + println(x.getClass()) // ok + println(x.isInstanceOf[Int]) // ok + x match + case x: Int => // error: should not be scrutinized + println("int") + x + case x: String => // error: should not be scrutinized + println("string") + x + List(x) match + case (x: Int) :: Nil => // error: should not be scrutinized + println("int") + x + case List(x: String) => // error: should not be scrutinized + println("string") + x + case List(y :: Nil) => // error: should not be scrutinized + y :: Nil + case _ => + x // error: should not be scrutinized + +@main def Test = + val x: Matchable = foo(1) + val y: Matchable = foo("hello") + assert(x != y) + diff --git a/tests/run/matchable.check b/tests/run/matchable.check new file mode 100644 index 000000000000..cdb965d49d92 --- /dev/null +++ b/tests/run/matchable.check @@ -0,0 +1,8 @@ +class java.lang.Integer +true +int +int +class java.lang.String +false +string +string diff --git a/tests/run/matchable.scala b/tests/run/matchable.scala new file mode 100644 index 000000000000..f4dbf58aa61a --- /dev/null +++ b/tests/run/matchable.scala @@ -0,0 +1,29 @@ +def foo[T](x: T): Matchable = + println(x.getClass()) + println(x.isInstanceOf[Int]) + x match + case x: Int => + println("int") + x + case x: String => + println("string") + x + List(x) match + case (x: Int) :: Nil => + println("int") + x + case List(x: String) => + println("string") + x + case List(y :: Nil) => + y :: Nil + case _ => + g(x) + +def g[T <: Matchable](x: T) = x + +@main def Test = + val x: Matchable = foo(1) + val y: Matchable = foo("hello") + assert(x != y) + From 2e77d342c0a3703fef9c4697821b26ffb940aa66 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Mon, 14 Dec 2020 13:56:47 +0100 Subject: [PATCH 4/9] Don't check for matchability if TypeTest is used --- compiler/src/dotty/tools/dotc/typer/Typer.scala | 10 +++++++--- .../fatal-warnings/type-test-matchable.scala | 13 +++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 tests/neg-custom-args/fatal-warnings/type-test-matchable.scala diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 7a6074f48020..90f057957c95 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -771,13 +771,17 @@ class Typer extends Namer def typedTpt = checkSimpleKinded(typedType(tree.tpt)) def handlePattern: Tree = { val tpt1 = typedTpt - if (!ctx.isAfterTyper && pt != defn.ImplicitScrutineeTypeRef) - checkMatchable(pt, tree.srcPos, pattern = true) + if !ctx.isAfterTyper && pt != defn.ImplicitScrutineeTypeRef then withMode(Mode.GadtConstraintInference) { TypeComparer.constrainPatternType(tpt1.tpe, pt) } + val matched = ascription(tpt1, isWildcard = true) // special case for an abstract type that comes with a class tag - tryWithTypeTest(ascription(tpt1, isWildcard = true), pt) + val result = tryWithTypeTest(ascribed, pt) + if (result eq matched) && pt != defn.ImplicitScrutineeTypeRef then + // no check for matchability if TestTest was applied + checkMatchable(pt, tree.srcPos, pattern = true) + result } cases( ifPat = handlePattern, diff --git a/tests/neg-custom-args/fatal-warnings/type-test-matchable.scala b/tests/neg-custom-args/fatal-warnings/type-test-matchable.scala new file mode 100644 index 000000000000..2008d0dcaf88 --- /dev/null +++ b/tests/neg-custom-args/fatal-warnings/type-test-matchable.scala @@ -0,0 +1,13 @@ +import scala.language.`3.1-migration` +import scala.reflect.TypeTest + +trait Foo: + type X + type Y <: X + def x: X + given TypeTest[X, Y] = ??? + +object Test: + def test(foo: Foo): Unit = + foo.x match + case x: foo.Y => From 0a288531d121f592237c01dc4350e32804f19bf9 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Mon, 14 Dec 2020 13:59:17 +0100 Subject: [PATCH 5/9] Update doc page --- .../reference/other-new-features/matchable.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/docs/reference/other-new-features/matchable.md b/docs/docs/reference/other-new-features/matchable.md index 68f152dd2b4e..e837f19bc281 100644 --- a/docs/docs/reference/other-new-features/matchable.md +++ b/docs/docs/reference/other-new-features/matchable.md @@ -63,6 +63,25 @@ extended by both `AnyVal` and `AnyRef`. Since `Matchable` is a supertype of ever - Type parameters and abstract types that are only bounded by some universal trait: Again, `Matchable` should be added as a bound. +Here is the hierarchy of toplevel classes and traits with their defined methods: +```scala +abstract class Any: + def getClass + def isInstanceOf + def asInstanceOf + def == + def != + def ## + def equals + def hashCode + def toString + +trait Matchable extends Any + +class AnyVal extends Any, Matchable +class Object extends Any, Matchable +``` + `Matchable` is currently a marker trait without any methods. Over time we might migrate methods `getClass` and `isInstanceOf` to it, since these are closely related to pattern-matching. From b2c2a86d0838c9a657f45541100851ee09bac66c Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Mon, 14 Dec 2020 14:05:06 +0100 Subject: [PATCH 6/9] Fix typo --- compiler/src/dotty/tools/dotc/typer/Typer.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 90f057957c95..3f82a960d9ff 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -777,7 +777,7 @@ class Typer extends Namer } val matched = ascription(tpt1, isWildcard = true) // special case for an abstract type that comes with a class tag - val result = tryWithTypeTest(ascribed, pt) + val result = tryWithTypeTest(matched, pt) if (result eq matched) && pt != defn.ImplicitScrutineeTypeRef then // no check for matchability if TestTest was applied checkMatchable(pt, tree.srcPos, pattern = true) From 8c4e45ac235fa2ee940cc5db7b4bc656c8fb6437 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Mon, 14 Dec 2020 15:23:36 +0100 Subject: [PATCH 7/9] Reclassify test --- .../fatal-warnings/type-test-matchable.scala | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{neg-custom-args => pos-special}/fatal-warnings/type-test-matchable.scala (100%) diff --git a/tests/neg-custom-args/fatal-warnings/type-test-matchable.scala b/tests/pos-special/fatal-warnings/type-test-matchable.scala similarity index 100% rename from tests/neg-custom-args/fatal-warnings/type-test-matchable.scala rename to tests/pos-special/fatal-warnings/type-test-matchable.scala From a389a1170d1b2c43b57c9ba5162cd8b603f6afaf Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Mon, 14 Dec 2020 16:28:27 +0100 Subject: [PATCH 8/9] Add test --- tests/pos-special/fatal-warnings/matchable-same-type.scala | 7 +++++++ tests/pos-special/fatal-warnings/type-test-matchable.scala | 1 + 2 files changed, 8 insertions(+) create mode 100644 tests/pos-special/fatal-warnings/matchable-same-type.scala diff --git a/tests/pos-special/fatal-warnings/matchable-same-type.scala b/tests/pos-special/fatal-warnings/matchable-same-type.scala new file mode 100644 index 000000000000..c6cee3fcdc48 --- /dev/null +++ b/tests/pos-special/fatal-warnings/matchable-same-type.scala @@ -0,0 +1,7 @@ +import scala.language.`3.1-migration` + +type X +def x: X = ??? +def test: Unit = + x match + case y: X => diff --git a/tests/pos-special/fatal-warnings/type-test-matchable.scala b/tests/pos-special/fatal-warnings/type-test-matchable.scala index 2008d0dcaf88..5becee6f2e9f 100644 --- a/tests/pos-special/fatal-warnings/type-test-matchable.scala +++ b/tests/pos-special/fatal-warnings/type-test-matchable.scala @@ -11,3 +11,4 @@ object Test: def test(foo: Foo): Unit = foo.x match case x: foo.Y => + case x: foo.X => From 7fad5c7bc3060d9628b08347789649a32ab56f7d Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Mon, 14 Dec 2020 19:34:53 +0100 Subject: [PATCH 9/9] Refine checking condition Don't check for matchability if - pattern type is a subtype of tested type - we are unapplying a typetest or classtag test --- compiler/src/dotty/tools/dotc/core/Mode.scala | 3 +++ .../src/dotty/tools/dotc/typer/Applications.scala | 3 ++- compiler/src/dotty/tools/dotc/typer/Typer.scala | 13 +++++++++---- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Mode.scala b/compiler/src/dotty/tools/dotc/core/Mode.scala index e982808b5738..99fbdc2621a9 100644 --- a/compiler/src/dotty/tools/dotc/core/Mode.scala +++ b/compiler/src/dotty/tools/dotc/core/Mode.scala @@ -116,4 +116,7 @@ object Mode { /** Are we typechecking the rhs of an extension method? */ val InExtensionMethod: Mode = newMode(26, "InExtensionMethod") + + /** Are we resolving a TypeTest node? */ + val InTypeTest: Mode = newMode(27, "InTypeTest") } diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index f461a493d661..ca763e6a8def 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -1158,7 +1158,8 @@ trait Applications extends Compatibility { def typedUnApply(tree: untpd.Apply, selType: Type)(using Context): Tree = { record("typedUnApply") val Apply(qual, args) = tree - checkMatchable(selType, tree.srcPos, pattern = true) + if !ctx.mode.is(Mode.InTypeTest) then + checkMatchable(selType, tree.srcPos, pattern = true) def notAnExtractor(tree: Tree): Tree = // prefer inner errors diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 3f82a960d9ff..b3fc6c639092 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -778,7 +778,10 @@ class Typer extends Namer val matched = ascription(tpt1, isWildcard = true) // special case for an abstract type that comes with a class tag val result = tryWithTypeTest(matched, pt) - if (result eq matched) && pt != defn.ImplicitScrutineeTypeRef then + if (result eq matched) + && pt != defn.ImplicitScrutineeTypeRef + && !(pt <:< tpt1.tpe) + then // no check for matchability if TestTest was applied checkMatchable(pt, tree.srcPos, pattern = true) result @@ -802,13 +805,15 @@ class Typer extends Namer inferImplicit(tpe, EmptyTree, tree.tpt.span) ) match case SearchSuccess(clsTag, _, _) => - Some(typed(untpd.Apply(untpd.TypedSplice(clsTag), untpd.TypedSplice(tree.expr)), pt)) + withMode(Mode.InTypeTest) { + Some(typed(untpd.Apply(untpd.TypedSplice(clsTag), untpd.TypedSplice(tree.expr)), pt)) + } case _ => None } val tag = withTag(defn.TypeTestClass.typeRef.appliedTo(pt, tref)) - .orElse(withTag(defn.ClassTagClass.typeRef.appliedTo(tref))) - .getOrElse(tree) + .orElse(withTag(defn.ClassTagClass.typeRef.appliedTo(tref))) + .getOrElse(tree) if tag.symbol.owner == defn.ClassTagClass && config.Feature.sourceVersion.isAtLeast(config.SourceVersion.`3.1`) then report.warning("Use of `scala.reflect.ClassTag` for type testing may be unsound. Consider using `scala.reflect.TypeTest` instead.", tree.srcPos) tag