From 352cc6f0030eeb0b9d811767aa7f1e8cf625aa9b Mon Sep 17 00:00:00 2001 From: Jamie Thompson Date: Tue, 31 May 2022 10:54:37 +0200 Subject: [PATCH] synthesize mirrors for small generic tuples, redux - handles generic tuples of different arity --- .../dotty/tools/dotc/core/TypeErasure.scala | 4 +- .../tools/dotc/printing/RefinedPrinter.scala | 2 +- .../dotc/transform/GenericSignatures.scala | 2 +- .../dotc/transform/SyntheticMembers.scala | 9 +++- .../tools/dotc/transform/TypeUtils.scala | 10 +++- .../dotty/tools/dotc/typer/Synthesizer.scala | 51 ++++++++++++++++--- .../src/dotty/tools/dotc/typer/Typer.scala | 2 +- tests/neg/i14127.check | 10 ++++ tests/neg/i14127.scala | 6 +++ tests/pos/i13859.scala | 7 +++ tests/run/i14127.scala | 14 +++++ tests/run/i7049.scala | 18 +++++++ 12 files changed, 121 insertions(+), 14 deletions(-) create mode 100644 tests/neg/i14127.check create mode 100644 tests/neg/i14127.scala create mode 100644 tests/run/i14127.scala create mode 100644 tests/run/i7049.scala diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index 2caa639592b3..87a829e51519 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -689,7 +689,9 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst } private def erasePair(tp: Type)(using Context): Type = { - val arity = tp.tupleArity + // NOTE: `tupleArity` does not consider TypeRef(EmptyTuple$) equivalent to EmptyTuple.type, + // we fix this for printers, but type erasure should be preserved. + val arity = tp.tupleArity() if (arity < 0) defn.ProductClass.typeRef else if (arity <= Definitions.MaxTupleArity) defn.TupleType(arity).nn else defn.TupleXXLClass.typeRef diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 22b098992a9f..9a9bc345ee69 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -218,7 +218,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { val cls = tycon.typeSymbol if tycon.isRepeatedParam then toTextLocal(args.head) ~ "*" else if defn.isFunctionClass(cls) then toTextFunction(args, cls.name.isContextFunction, cls.name.isErasedFunction) - else if tp.tupleArity >= 2 && !printDebug then toTextTuple(tp.tupleElementTypes) + else if tp.tupleArity(relaxEmptyTuple = true) >= 2 && !printDebug then toTextTuple(tp.tupleElementTypes) else if isInfixType(tp) then val l :: r :: Nil = args: @unchecked val opName = tyconName(tycon) diff --git a/compiler/src/dotty/tools/dotc/transform/GenericSignatures.scala b/compiler/src/dotty/tools/dotc/transform/GenericSignatures.scala index 9a6ab233e239..6a01c9dc64f9 100644 --- a/compiler/src/dotty/tools/dotc/transform/GenericSignatures.scala +++ b/compiler/src/dotty/tools/dotc/transform/GenericSignatures.scala @@ -248,7 +248,7 @@ object GenericSignatures { case _ => jsig(elemtp) case RefOrAppliedType(sym, pre, args) => - if (sym == defn.PairClass && tp.tupleArity > Definitions.MaxTupleArity) + if (sym == defn.PairClass && tp.tupleArity() > Definitions.MaxTupleArity) jsig(defn.TupleXXLClass.typeRef) else if (isTypeParameterInSig(sym, sym0)) { assert(!sym.isAliasType, "Unexpected alias type: " + sym) diff --git a/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala b/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala index faccb79f3c9a..575c416384b3 100644 --- a/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala +++ b/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala @@ -26,6 +26,9 @@ object SyntheticMembers { /** Attachment recording that an anonymous class should extend Mirror.Sum */ val ExtendsSumMirror: Property.StickyKey[Unit] = new Property.StickyKey + + /** Attachment recording that an anonymous class should extend Mirror.Sum */ + val GenericTupleArity: Property.StickyKey[Int] = new Property.StickyKey } /** Synthetic method implementations for case classes, case objects, @@ -601,7 +604,11 @@ class SyntheticMembers(thisPhase: DenotTransformer) { else if (impl.removeAttachment(ExtendsSingletonMirror).isDefined) makeSingletonMirror() else if (impl.removeAttachment(ExtendsProductMirror).isDefined) - makeProductMirror(monoType.typeRef.dealias.classSymbol) + val tupleArity = impl.removeAttachment(GenericTupleArity) + val cls = tupleArity match + case Some(n) => defn.TupleType(n).nn.classSymbol + case _ => monoType.typeRef.dealias.classSymbol + makeProductMirror(cls) else if (impl.removeAttachment(ExtendsSumMirror).isDefined) makeSumMirror(monoType.typeRef.dealias.classSymbol) diff --git a/compiler/src/dotty/tools/dotc/transform/TypeUtils.scala b/compiler/src/dotty/tools/dotc/transform/TypeUtils.scala index fba81fec632c..2bf72ec588b8 100644 --- a/compiler/src/dotty/tools/dotc/transform/TypeUtils.scala +++ b/compiler/src/dotty/tools/dotc/transform/TypeUtils.scala @@ -51,13 +51,17 @@ object TypeUtils { /** The arity of this tuple type, which can be made up of EmptyTuple, TupleX and `*:` pairs, * or -1 if this is not a tuple type. + * + * @param relaxEmptyTuple if true then TypeRef(EmptyTuple$) =:= EmptyTuple.type */ - def tupleArity(using Context): Int = self match { + def tupleArity(relaxEmptyTuple: Boolean = false)(using Context): Int = self match { case AppliedType(tycon, _ :: tl :: Nil) if tycon.isRef(defn.PairClass) => - val arity = tl.tupleArity + val arity = tl.tupleArity(relaxEmptyTuple) if (arity < 0) arity else arity + 1 case self: SingletonType => if self.termSymbol == defn.EmptyTupleModule then 0 else -1 + case self: TypeRef if relaxEmptyTuple && self.classSymbol == defn.EmptyTupleModule.moduleClass => + 0 case self if defn.isTupleClass(self.classSymbol) => self.dealias.argInfos.length case _ => @@ -71,6 +75,8 @@ object TypeUtils { case self: SingletonType => assert(self.termSymbol == defn.EmptyTupleModule, "not a tuple") Nil + case self: TypeRef if self.classSymbol == defn.EmptyTupleModule.moduleClass => + Nil case self if defn.isTupleClass(self.classSymbol) => self.dealias.argInfos case _ => diff --git a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala index 1267000d9733..4364b1c69307 100644 --- a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala @@ -223,16 +223,19 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): /** Create an anonymous class `new Object { type MirroredMonoType = ... }` * and mark it with given attachment so that it is made into a mirror at PostTyper. */ - private def anonymousMirror(monoType: Type, attachment: Property.StickyKey[Unit], span: Span)(using Context) = + private def anonymousMirror(monoType: Type, attachment: Property.StickyKey[Unit], tupleArity: Option[Int], span: Span)(using Context) = if ctx.isAfterTyper then ctx.compilationUnit.needsMirrorSupport = true val monoTypeDef = untpd.TypeDef(tpnme.MirroredMonoType, untpd.TypeTree(monoType)) - val newImpl = untpd.Template( + var newImpl = untpd.Template( constr = untpd.emptyConstructor, parents = untpd.TypeTree(defn.ObjectType) :: Nil, derived = Nil, self = EmptyValDef, body = monoTypeDef :: Nil ).withAttachment(attachment, ()) + tupleArity.foreach { n => + newImpl = newImpl.withAttachment(GenericTupleArity, n) + } typer.typed(untpd.New(newImpl).withSpan(span)) /** The mirror type @@ -279,6 +282,17 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): private[Synthesizer] enum MirrorSource: case ClassSymbol(cls: Symbol) case Singleton(src: Symbol, tref: TermRef) + case GenericTuple(tps: List[Type]) + + /** Tests that both sides are tuples of the same arity */ + infix def sameTuple(that: MirrorSource)(using Context): Boolean = + def arity(msrc: MirrorSource): Int = msrc match + case GenericTuple(tps) => tps.size + case ClassSymbol(cls) if defn.isTupleClass(cls) => cls.typeParams.size // tested in tests/pos/i13859.scala + case _ => -1 + def equivalent(n: Int, m: Int) = + n == m && n > 0 + equivalent(arity(this), arity(that)) /** A comparison that chooses the most specific MirrorSource, this is guided by what is necessary for * `Mirror.Product.fromProduct`. i.e. its result type should be compatible with the erasure of `mirroredType`. @@ -289,10 +303,15 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): case (ClassSymbol(cls1), ClassSymbol(cls2)) => cls1.isSubClass(cls2) case (Singleton(src1, _), Singleton(src2, _)) => src1 eq src2 case (_: ClassSymbol, _: Singleton) => false + case _ => this sameTuple that def show(using Context): String = this match case ClassSymbol(cls) => i"$cls" case Singleton(src, _) => i"$src" + case GenericTuple(tps) => + val arity = tps.size + if arity <= Definitions.MaxTupleArity then s"class Tuple$arity" + else s"trait Tuple { def size: $arity }" private[Synthesizer] object MirrorSource: @@ -332,6 +351,11 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): reduce(tp.underlying) case tp: HKTypeLambda if tp.resultType.isInstanceOf[HKTypeLambda] => Left(i"its subpart `$tp` is not a supported kind (either `*` or `* -> *`)") + case tp @ AppliedType(tref: TypeRef, _) + if tref.symbol == defn.PairClass + && tp.tupleArity(relaxEmptyTuple = true) > 0 => + // avoid type aliases for tuples + Right(MirrorSource.GenericTuple(tp.tupleElementTypes)) case tp: TypeProxy => reduce(tp.underlying) case tp @ AndType(l, r) => @@ -354,10 +378,12 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): private def productMirror(mirroredType: Type, formal: Type, span: Span)(using Context): TreeWithErrors = - def makeProductMirror(cls: Symbol): TreeWithErrors = + def makeProductMirror(cls: Symbol, tps: Option[List[Type]]): TreeWithErrors = val accessors = cls.caseAccessors.filterNot(_.isAllOf(PrivateLocal)) val elemLabels = accessors.map(acc => ConstantType(Constant(acc.name.toString))) - val nestedPairs = TypeOps.nestedPairs(accessors.map(mirroredType.resultType.memberInfo(_).widenExpr)) + val nestedPairs = + val elems = tps.getOrElse(accessors.map(mirroredType.resultType.memberInfo(_).widenExpr)) + TypeOps.nestedPairs(elems) val (monoType, elemsType) = mirroredType match case mirroredType: HKTypeLambda => (mkMirroredMonoType(mirroredType), mirroredType.derivedLambdaType(resType = nestedPairs)) @@ -372,7 +398,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): .refinedWith(tpnme.MirroredElemLabels, TypeAlias(elemsLabels)) val mirrorRef = if cls.useCompanionAsProductMirror then companionPath(mirroredType, span) - else anonymousMirror(monoType, ExtendsProductMirror, span) + else anonymousMirror(monoType, ExtendsProductMirror, tps.map(_.size), span) withNoErrors(mirrorRef.cast(mirrorType)) end makeProductMirror @@ -389,8 +415,15 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): else val mirrorType = mirrorCore(defn.Mirror_SingletonClass, mirroredType, mirroredType, singleton.name, formal) withNoErrors(singletonPath.cast(mirrorType)) + case MirrorSource.GenericTuple(tps) => + val maxArity = Definitions.MaxTupleArity + val arity = tps.size + if tps.size <= maxArity then makeProductMirror(defn.TupleType(arity).nn.classSymbol, Some(tps)) + else + val reason = s"it reduces to a tuple with arity $arity, expected arity <= $maxArity" + withErrors(i"${defn.PairClass} is not a generic product because $reason") case MirrorSource.ClassSymbol(cls) => - if cls.isGenericProduct then makeProductMirror(cls) + if cls.isGenericProduct then makeProductMirror(cls, None) else withErrors(i"$cls is not a generic product because ${cls.whyNotGenericProduct}") case Left(msg) => withErrors(i"type `$mirroredType` is not a generic product because $msg") @@ -401,6 +434,10 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): val (acceptableMsg, cls) = MirrorSource.reduce(mirroredType) match case Right(MirrorSource.Singleton(_, tp)) => (i"its subpart `$tp` is a term reference", NoSymbol) case Right(MirrorSource.ClassSymbol(cls)) => ("", cls) + case Right(MirrorSource.GenericTuple(tps)) => + val arity = tps.size + val cls = if arity <= Definitions.MaxTupleArity then defn.TupleType(arity).nn.classSymbol else defn.PairClass + ("", cls) case Left(msg) => (msg, NoSymbol) val clsIsGenericSum = cls.isGenericSum @@ -457,7 +494,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): .refinedWith(tpnme.MirroredElemLabels, TypeAlias(TypeOps.nestedPairs(elemLabels))) val mirrorRef = if cls.useCompanionAsSumMirror then companionPath(mirroredType, span) - else anonymousMirror(monoType, ExtendsSumMirror, span) + else anonymousMirror(monoType, ExtendsSumMirror, None, span) withNoErrors(mirrorRef.cast(mirrorType)) else if acceptableMsg.nonEmpty then withErrors(i"type `$mirroredType` is not a generic sum because $acceptableMsg") diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index a0058233588a..9b7db49f8843 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2767,7 +2767,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer typed(desugar.smallTuple(tree).withSpan(tree.span), pt) else { val pts = - if (arity == pt.tupleArity) pt.tupleElementTypes + if (arity == pt.tupleArity()) pt.tupleElementTypes else List.fill(arity)(defn.AnyType) val elems = tree.trees.lazyZip(pts).map( if ctx.mode.is(Mode.Type) then typedType(_, _, mapPatternBounds = true) diff --git a/tests/neg/i14127.check b/tests/neg/i14127.check new file mode 100644 index 000000000000..969092401012 --- /dev/null +++ b/tests/neg/i14127.check @@ -0,0 +1,10 @@ +-- Error: tests/neg/i14127.scala:6:55 ---------------------------------------------------------------------------------- +6 | *: Int *: Int *: Int *: Int *: Int *: EmptyTuple)]] // error + | ^ + |No given instance of type deriving.Mirror.Of[(Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, + | Int + |, Int, Int)] was found for parameter x of method summon in object Predef. Failed to synthesize an instance of type deriving.Mirror.Of[(Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, + | Int + |, Int, Int)]: + | * class *: is not a generic product because it reduces to a tuple with arity 23, expected arity <= 22 + | * class *: is not a generic sum because it does not have subclasses diff --git a/tests/neg/i14127.scala b/tests/neg/i14127.scala new file mode 100644 index 000000000000..40e23d739898 --- /dev/null +++ b/tests/neg/i14127.scala @@ -0,0 +1,6 @@ +import scala.deriving.Mirror + +val mT23 = summon[Mirror.Of[( + Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int + *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int + *: Int *: Int *: Int *: Int *: Int *: EmptyTuple)]] // error diff --git a/tests/pos/i13859.scala b/tests/pos/i13859.scala index 4092de52fd94..b3104f10aa38 100644 --- a/tests/pos/i13859.scala +++ b/tests/pos/i13859.scala @@ -29,3 +29,10 @@ object Test: type MirroredElemTypes[X, Y] = (Y, X) } } + + locally { + val x = summon[Kind2[Mirror.Product, [X, Y] =>> (Y, X) & (Y *: X *: EmptyTuple)]] + x: Mirror.Product { + type MirroredElemTypes[X, Y] = (Y, X) + } + } diff --git a/tests/run/i14127.scala b/tests/run/i14127.scala new file mode 100644 index 000000000000..d033c462b149 --- /dev/null +++ b/tests/run/i14127.scala @@ -0,0 +1,14 @@ +import scala.deriving.Mirror + +@main def Test = + val mISB = summon[Mirror.Of[Int *: String *: Boolean *: EmptyTuple]] + assert(mISB.fromProduct((1, "foo", true)) == (1, "foo", true)) + + val mT22 = summon[Mirror.Of[( + Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int + *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int + *: Int *: Int *: Int *: Int *: EmptyTuple)]] + + // tuple of 22 elements + val t22 = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22) + assert(mT22.fromProduct(t22) == t22) diff --git a/tests/run/i7049.scala b/tests/run/i7049.scala new file mode 100644 index 000000000000..f81c642c408d --- /dev/null +++ b/tests/run/i7049.scala @@ -0,0 +1,18 @@ +import scala.deriving._ + +case class Foo(x: Int, y: String) + +def toTuple[T <: Product](x: T)(using m: Mirror.ProductOf[T], mt: Mirror.ProductOf[m.MirroredElemTypes]) = + mt.fromProduct(x) + +@main def Test = { + val m = summon[Mirror.ProductOf[Foo]] + val mt1 = summon[Mirror.ProductOf[(Int, String)]] + type R = (Int, String) + val mt2 = summon[Mirror.ProductOf[R]] + val mt3 = summon[Mirror.ProductOf[m.MirroredElemTypes]] + + val f = Foo(1, "foo") + val g: (Int, String) = toTuple(f)// (using m, mt1) + assert(g == (1, "foo")) +}