diff --git a/compiler/src/dotty/tools/dotc/core/CheckRealizable.scala b/compiler/src/dotty/tools/dotc/core/CheckRealizable.scala index 060189016828..d8241f3ff304 100644 --- a/compiler/src/dotty/tools/dotc/core/CheckRealizable.scala +++ b/compiler/src/dotty/tools/dotc/core/CheckRealizable.scala @@ -116,15 +116,7 @@ class CheckRealizable(using Context) { case _: SingletonType | NoPrefix => Realizable case tp => - def isConcrete(tp: Type): Boolean = tp.dealias match { - case tp: TypeRef => tp.symbol.isClass - case tp: TypeParamRef => false - case tp: TypeProxy => isConcrete(tp.underlying) - case tp: AndType => isConcrete(tp.tp1) && isConcrete(tp.tp2) - case tp: OrType => isConcrete(tp.tp1) && isConcrete(tp.tp2) - case _ => false - } - if (!isConcrete(tp)) NotConcrete + if !MatchTypes.isConcrete(tp) then NotConcrete else boundsRealizability(tp).andAlso(memberRealizability(tp)) } diff --git a/compiler/src/dotty/tools/dotc/core/MatchTypes.scala b/compiler/src/dotty/tools/dotc/core/MatchTypes.scala new file mode 100644 index 000000000000..a3becea40886 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/core/MatchTypes.scala @@ -0,0 +1,47 @@ +package dotty.tools +package dotc +package core + +import Types.*, Contexts.*, Symbols.*, Flags.*, Decorators.* + +object MatchTypes: + + /* Concreteness checking + * + * When following a baseType and reaching a non-wildcard, in-variant-pos type capture, + * we have to make sure that the scrutinee is concrete enough to uniquely determine + * the values of the captures. This comes down to checking that we do not follow any + * upper bound of an abstract type. + * + * See notably neg/wildcard-match.scala for examples of this. + * + * See neg/i13780.scala, neg/i13780-1.scala and neg/i19746.scala for + * ClassCastException reproducers if we disable this check. + */ + def isConcrete(tp: Type)(using Context): Boolean = + val tp1 = tp.normalized + + tp1 match + case tp1: TypeRef => + if tp1.symbol.isClass then true + else + tp1.info match + case info: AliasingBounds => isConcrete(info.alias) + case _ => false + case tp1: AppliedType => + isConcrete(tp1.tycon) && isConcrete(tp1.superType) + case tp1: HKTypeLambda => + true + case tp1: TermRef => + !tp1.symbol.is(Param) && isConcrete(tp1.underlying) + case _: (ParamRef | MatchType) => + false + case tp1: TypeProxy => + isConcrete(tp1.underlying) + case tp1: AndOrType => + isConcrete(tp1.tp1) && isConcrete(tp1.tp2) + case _ => + false + end isConcrete + +end MatchTypes \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 140b42e0e9a9..c53c2238a095 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -24,6 +24,7 @@ import reporting.trace import annotation.constructorOnly import cc.* import NameKinds.WildcardParamName +import MatchTypes.isConcrete /** Provides methods to compare types. */ @@ -3409,58 +3410,6 @@ class MatchReducer(initctx: Context) extends TypeComparer(initctx) { // See https://docs.scala-lang.org/sips/match-types-spec.html#matching def matchSpeccedPatMat(spec: MatchTypeCaseSpec.SpeccedPatMat): MatchResult = - /* Concreteness checking - * - * When following a baseType and reaching a non-wildcard, in-variant-pos type capture, - * we have to make sure that the scrutinee is concrete enough to uniquely determine - * the values of the captures. This comes down to checking that we do not follow any - * upper bound of an abstract type. - * - * See notably neg/wildcard-match.scala for examples of this. - * - * See neg/i13780.scala, neg/i13780-1.scala and neg/i19746.scala for - * ClassCastException reproducers if we disable this check. - */ - - def isConcrete(tp: Type): Boolean = - val tp1 = tp.normalized - - tp1 match - case tp1: TypeRef => - if tp1.symbol.isClass then true - else - tp1.info match - case info: AliasingBounds => isConcrete(info.alias) - case _ => false - case tp1: AppliedType => - isConcrete(tp1.tycon) && isConcrete(tp1.superType) - case tp1: HKTypeLambda => - true - case tp1: TermRef => - !tp1.symbol.is(Param) && isConcrete(tp1.underlying) - case tp1: TermParamRef => - false - case tp1: SingletonType => - isConcrete(tp1.underlying) - case tp1: ExprType => - isConcrete(tp1.underlying) - case tp1: AnnotatedType => - isConcrete(tp1.parent) - case tp1: RefinedType => - isConcrete(tp1.underlying) - case tp1: RecType => - isConcrete(tp1.underlying) - case tp1: AndOrType => - isConcrete(tp1.tp1) && isConcrete(tp1.tp2) - case tp1: FlexibleType => - isConcrete(tp1.hi) - case _ => - val tp2 = tp1.stripped.stripLazyRef - (tp2 ne tp) && isConcrete(tp2) - end isConcrete - - // Actual matching logic - val instances = Array.fill[Type](spec.captureCount)(NoType) val noInstances = mutable.ListBuffer.empty[(TypeName, TypeBounds)] diff --git a/compiler/src/dotty/tools/dotc/core/TypeEval.scala b/compiler/src/dotty/tools/dotc/core/TypeEval.scala index af4f1e0153dd..4d5496cff880 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeEval.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeEval.scala @@ -101,7 +101,7 @@ object TypeEval: expectArgsNum(1) val arg = tp.args.head val cls = arg.classSymbol - if cls.is(CaseClass) then + if MatchTypes.isConcrete(arg) && cls.is(CaseClass) then val fields = cls.caseAccessors val fieldLabels = fields.map: field => ConstantType(Constant(field.name.toString)) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index aa1813f572f7..efb353c4050c 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -76,6 +76,7 @@ object Types extends TypeUtils { * | +- HKTypeLambda * | +- MatchType * | +- FlexibleType + * | +- LazyRef * | * +- GroundType -+- AndType * +- OrType diff --git a/tests/neg/i20517.check b/tests/neg/i20517.check new file mode 100644 index 000000000000..55aeff46572b --- /dev/null +++ b/tests/neg/i20517.check @@ -0,0 +1,7 @@ +-- [E007] Type Mismatch Error: tests/neg/i20517.scala:10:43 ------------------------------------------------------------ +10 | def dep(foo: Foo[Any]): From[foo.type] = (elem = "") // error + | ^^^^^^^^^^^ + | Found: (elem : String) + | Required: NamedTuple.From[(foo : Foo[Any])] + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/i20517.scala b/tests/neg/i20517.scala new file mode 100644 index 000000000000..11c4432434dd --- /dev/null +++ b/tests/neg/i20517.scala @@ -0,0 +1,17 @@ +import scala.language.experimental.namedTuples +import NamedTuple.From + +case class Foo[+T](elem: T) + +trait Base[M[_]]: + def dep(foo: Foo[Any]): M[foo.type] + +class SubAny extends Base[From]: + def dep(foo: Foo[Any]): From[foo.type] = (elem = "") // error + +object Test: + @main def run = + val f: Foo[Int] = Foo(elem = 1) + val b: Base[From] = SubAny() + val nt: (elem: Int) = b.dep(f) + val x: Int = nt.elem // was ClassCastException \ No newline at end of file