From 532a9daab3cfda88ee11a7fa2df6eb50551b7ad7 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 2 Jul 2024 14:21:28 +0200 Subject: [PATCH 1/2] Re-use isConcrete checking in match types for NamedTyple.From - Move isConcrete to a new object `MatchTypes`. We should also move other MatchType-related stuff from Types and TypeComparer here. Type and TypeComparer are already unconfortably big, and MatchTypes are a coherent topic where everything should work together. - Streamline isConcrete a bit. - Re-use isConcrete for a similar test in CheckRealizable. - Re-use isConcrete for evaluating NamedTuple.From Fixes #20517 --- .../tools/dotc/core/CheckRealizable.scala | 10 +--- .../dotty/tools/dotc/core/MatchTypes.scala | 59 +++++++++++++++++++ .../dotty/tools/dotc/core/TypeComparer.scala | 53 +---------------- .../src/dotty/tools/dotc/core/TypeEval.scala | 2 +- tests/neg/i20517.check | 7 +++ tests/neg/i20517.scala | 17 ++++++ 6 files changed, 86 insertions(+), 62 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/core/MatchTypes.scala create mode 100644 tests/neg/i20517.check create mode 100644 tests/neg/i20517.scala 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..61caceccd5d4 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/core/MatchTypes.scala @@ -0,0 +1,59 @@ +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 tp1: TermParamRef => + false + case tp1: SingletonType => + isConcrete(tp1.underlying) + case tp1: ExprType => + isConcrete(tp1.underlying) + case tp1: AnnotatedType => + isConcrete(tp1.parent) + case tp1: RefinedOrRecType => + isConcrete(tp1.underlying) + case tp1: AndOrType => + isConcrete(tp1.tp1) && isConcrete(tp1.tp2) + case tp1: TypeVar => + isConcrete(tp1.underlying) + case tp1: LazyRef => + isConcrete(tp1.ref) + case tp1: FlexibleType => + isConcrete(tp1.hi) + 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/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 From de34efebb9e9f5238147cd1434da8e406285e78c Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 2 Jul 2024 14:30:05 +0200 Subject: [PATCH 2/2] Streamline isConcrete a bit more --- .../src/dotty/tools/dotc/core/MatchTypes.scala | 16 ++-------------- compiler/src/dotty/tools/dotc/core/Types.scala | 1 + 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/MatchTypes.scala b/compiler/src/dotty/tools/dotc/core/MatchTypes.scala index 61caceccd5d4..a3becea40886 100644 --- a/compiler/src/dotty/tools/dotc/core/MatchTypes.scala +++ b/compiler/src/dotty/tools/dotc/core/MatchTypes.scala @@ -34,24 +34,12 @@ object MatchTypes: true case tp1: TermRef => !tp1.symbol.is(Param) && isConcrete(tp1.underlying) - case tp1: TermParamRef => + case _: (ParamRef | MatchType) => false - case tp1: SingletonType => - isConcrete(tp1.underlying) - case tp1: ExprType => - isConcrete(tp1.underlying) - case tp1: AnnotatedType => - isConcrete(tp1.parent) - case tp1: RefinedOrRecType => + case tp1: TypeProxy => isConcrete(tp1.underlying) case tp1: AndOrType => isConcrete(tp1.tp1) && isConcrete(tp1.tp2) - case tp1: TypeVar => - isConcrete(tp1.underlying) - case tp1: LazyRef => - isConcrete(tp1.ref) - case tp1: FlexibleType => - isConcrete(tp1.hi) case _ => false end isConcrete 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