Skip to content

Synthesize Representable type class #3663

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 32 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
4d80935
Register children in Typed instead of PostTyper
OlivierBlanvillain Nov 17, 2017
1e7ab29
Add skeleton of generic library
OlivierBlanvillain Nov 15, 2017
e08a98c
Add implicit search hook
OlivierBlanvillain Nov 15, 2017
800bc05
Synthesise Repr type alias for products
OlivierBlanvillain Nov 16, 2017
40a8ee4
Implement selectorName using productAccessorName
OlivierBlanvillain Nov 17, 2017
659ec4e
Synthesise to and from for products
OlivierBlanvillain Nov 17, 2017
8936872
Add generic-sum test
OlivierBlanvillain Nov 21, 2017
f43fa1a
Synthesise type Repr for sums
OlivierBlanvillain Nov 21, 2017
5a2726e
Synthesise to/from for sums
OlivierBlanvillain Nov 22, 2017
89ce33f
Simplify Product.from using a Match instead
OlivierBlanvillain Nov 22, 2017
3263d44
Document sum case
OlivierBlanvillain Nov 22, 2017
01742e5
Rewrite sum.to using a match
OlivierBlanvillain Nov 23, 2017
145ca11
Refactoring, root qualify names form dotty.generic._
OlivierBlanvillain Nov 23, 2017
450efee
Wip porting shapeless tests
OlivierBlanvillain Nov 24, 2017
a585c36
Fix "_ is not a type name" on objects
OlivierBlanvillain Nov 24, 2017
d6e1e75
Add asSeenFrom for sums
OlivierBlanvillain Nov 27, 2017
3ddb304
Swap product/sum cases in pattern match
OlivierBlanvillain Nov 30, 2017
471fb3f
Handle case objects
OlivierBlanvillain Nov 30, 2017
8fbb760
Handle varargs
OlivierBlanvillain Nov 30, 2017
e461835
Remove warnings using unchecked
OlivierBlanvillain Nov 30, 2017
175f022
Reuse instantiate from patmat exhaustivity
OlivierBlanvillain Dec 1, 2017
86b3a16
Update representable tests
OlivierBlanvillain Dec 1, 2017
a33dcfa
Use lower kinded Sum/Prod for Representatble
OlivierBlanvillain Dec 4, 2017
1bd9e71
Use fullyDefinedType to synthesise type arguments
OlivierBlanvillain Dec 4, 2017
e35ef67
Adapt to make typer aware of the new contraints
OlivierBlanvillain Dec 6, 2017
09eb241
Update representable.scala
OlivierBlanvillain Dec 6, 2017
62c0037
Replace rootQual by tpd.ref :)
OlivierBlanvillain Dec 7, 2017
a24612b
Fix a genLoad bug with .widen
OlivierBlanvillain Dec 8, 2017
e53e690
Split synthesizedRepresentable into 3 methods
OlivierBlanvillain Dec 8, 2017
ab52281
Strip "../" from test output
OlivierBlanvillain Sep 19, 2017
460cf6d
Cleanup
OlivierBlanvillain Dec 12, 2017
2858e53
Add documentation
OlivierBlanvillain Dec 12, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,29 @@ class Definitions {

def Not_value(implicit ctx: Context) = NotModule.requiredMethod(nme.value)

// Generic classes
lazy val RepresentableType = ctx.requiredClassRef("dotty.generic.Representable")
def RepresentableClass(implicit ctx: Context) = RepresentableType.symbol.asClass
def RepresentableModule(implicit ctx: Context) = RepresentableClass.companionModule
lazy val PNilType = ctx.requiredClassRef("dotty.generic.PNil")
def PNilClass(implicit ctx: Context) = PNilType.symbol.asClass
def PNilModule(implicit ctx: Context) = PNilClass.companionModule
lazy val PConsType = ctx.requiredClassRef("dotty.generic.PCons")
def PConsClass(implicit ctx: Context) = PConsType.symbol.asClass
def PConsModule(implicit ctx: Context) = PConsClass.companionModule
lazy val SNilType = ctx.requiredClassRef("dotty.generic.SNil")
def SNilClass(implicit ctx: Context) = SNilType.symbol.asClass
def SNilModule(implicit ctx: Context) = SNilClass.companionModule
lazy val SConsType = ctx.requiredClassRef("dotty.generic.SCons")
def SConsClass(implicit ctx: Context) = SConsType.symbol.asClass
def SConsModule(implicit ctx: Context) = SConsClass.companionModule
lazy val SLeftType = ctx.requiredClassRef("dotty.generic.SLeft")
def SLeftClass(implicit ctx: Context) = SLeftType.symbol.asClass
def SLeftModule(implicit ctx: Context) = SLeftClass.companionModule
lazy val SRightType = ctx.requiredClassRef("dotty.generic.SRight")
def SRightClass(implicit ctx: Context) = SRightType.symbol.asClass
def SRightModule(implicit ctx: Context) = SRightClass.companionModule

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

// Annotation base classes
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/core/StdNames.scala
Original file line number Diff line number Diff line change
Expand Up @@ -723,7 +723,7 @@ object StdNames {

def newBitmapName(bitmapPrefix: TermName, n: Int): TermName = bitmapPrefix ++ n.toString

def selectorName(n: Int): TermName = "_" + (n + 1)
def selectorName(n: Int): TermName = productAccessorName(n + 1)

object primitive {
val arrayApply: TermName = "[]apply"
Expand Down
13 changes: 4 additions & 9 deletions compiler/src/dotty/tools/dotc/transform/PostTyper.scala
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,10 @@ import reporting.diagnostic.messages.{NotAMember, SuperCallsNotAllowedInline}
*
* (9) Adds SourceFile annotations to all top-level classes and objects
*
* (10) Adds Child annotations to all sealed classes
*
* (11) Minimizes `call` fields of `Inline` nodes to just point to the toplevel
* (10) Minimizes `call` fields of `Inline` nodes to just point to the toplevel
* class from which code was inlined.
*
* (12) Converts GADT bounds into normal type bounds
* (11) Converts GADT bounds into normal type bounds
*
* The reason for making this a macro transform is that some functions (in particular
* super and protected accessors and instantiation checks) are naturally top-down and
Expand Down Expand Up @@ -104,11 +102,8 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase
private def transformAnnot(annot: Annotation)(implicit ctx: Context): Annotation =
annot.derivedAnnotation(transformAnnot(annot.tree))

private def transformMemberDef(tree: MemberDef)(implicit ctx: Context): Unit = {
val sym = tree.symbol
sym.registerIfChild()
sym.transformAnnotations(transformAnnot)
}
private def transformMemberDef(tree: MemberDef)(implicit ctx: Context): Unit =
tree.symbol.transformAnnotations(transformAnnot)

private def transformSelect(tree: Select, targs: List[Tree])(implicit ctx: Context): Tree = {
val qual = tree.qualifier
Expand Down
166 changes: 164 additions & 2 deletions compiler/src/dotty/tools/dotc/typer/Implicits.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import config.Config
import config.Printers.{implicits, implicitsDetailed, typr}
import collection.mutable
import reporting.trace
import annotation.tailrec
import transform.SymUtils._

/** Implicit resolution */
object Implicits {
Expand Down Expand Up @@ -562,7 +564,7 @@ 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)(implicit ctx: Context): Tree =
formal.argTypes match {
case arg :: Nil =>
Expand All @@ -585,7 +587,7 @@ trait Implicits { self: Typer =>

/** 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 {
Expand All @@ -598,6 +600,164 @@ trait Implicits { self: Typer =>
}
}

/** Synthesized dotty.generic.Representable instance when `formal` is
* a case class or of sealed class.
*/
def synthesizedRepresentable(formal: Type)(implicit ctx: Context): Tree = {
val generated = formal.baseType(defn.RepresentableClass).argTypes match {
case (arg @ _) :: Nil =>
val A = fullyDefinedType(arg, "Representable argument", pos).stripTypeVar
if (A.typeSymbol.is(Sealed))
synthesizedRepresentableSum(A)
else if (defn.isProductSubType(A))
synthesizedRepresentableProduct(A)
else
EmptyTree
case _ => EmptyTree
}

val typed = typedUnadapted(generated)
adapt(typed, formal)
}

/** Assuming A is a sum, synthesize a Representable instance of the following shape:
*
* ```
* new Representable[A] {
* type Repr = SCons[T1, SCons[T2, ..., SCons[Tn, SNil]]]
*
* def to(a: A): Repr = (a: @unchecked) match {
* case x: T1 => SLeft(x)
* case x: T2 => SRight(SLeft(x))
* ...
* case x: Tn => SRight(SRight(...SRight(SLeft(x))))
* }
*
* def from(r: Repr): A = (r: @unchecked) match {
* case SLeft(x) => x
* case SRight(SLeft(x)) => x
* ...
* case SRight(SRight(...SRight(SLeft(x)))) => x
* }
* ```
*/
def synthesizedRepresentableSum(A: Type): untpd.Tree = {
import untpd._
// TODO: Factor SpaceEngine.instantiate out of pattern matching logic.
// I'm not sure where would be the best place to put it...
import dotty.tools.dotc.transform.patmat.SpaceEngine

val sumTypes =
A .classSymbol.children.map(_.namedType)
.map(t => new SpaceEngine().instantiate(t, A)) // Instantiate type parameters
.reverse // Reveresed to match source order

val sumTypesSize = sumTypes.size
val noBounds = TypeBoundsTree(EmptyTree, EmptyTree)

val repr = TypeDef("Repr".toTypeName,
sumTypes.zipWithIndex.foldRight[Tree](TypeTree(defn.SNilType)) { case ((cur, i), acc) =>
AppliedTypeTree(TypeTree(defn.SConsType), TypeTree(cur) :: acc :: Nil)
}
)

def unchecked(t: Tree): Tree = Annotated(t, untpd.New(TypeTree(defn.UncheckedAnnotType), Nil))

val to: Tree = {
val arg = makeSyntheticParameter(tpt = TypeTree(A))
val cases = {
val x = nme.syntheticParamName(0)
sumTypes.zipWithIndex.map { case (tpe, depth) =>
val rhs = (1 to depth).foldLeft[Tree](Apply(tpd.ref(defn.SLeftModule), Ident(x))) {
case (acc, _) => Apply(tpd.ref(defn.SRightModule), acc)
}
CaseDef(Typed(Ident(x), TypeTree(tpe.widen)), EmptyTree, rhs)
}
}
val body = Match(unchecked(Ident(arg.name)), cases)
DefDef("to".toTermName, Nil, (arg :: Nil) :: Nil, Ident(repr.name), body).withFlags(Synthetic)
}

val from: Tree = {
val arg = makeSyntheticParameter(tpt = Ident(repr.name))
val cases = {
val x = nme.syntheticParamName(0)
(1 to sumTypesSize).map { depth =>
val pat = (1 until depth).foldLeft(Apply(tpd.ref(defn.SLeftModule), Ident(x))) {
case (acc, _) => Apply(tpd.ref(defn.SRightModule), acc)
}
CaseDef(pat, EmptyTree, Ident(x))
}.toList
}
val matsh = Match(unchecked(Ident(arg.name)), cases)
val body = TypeApply(Select(matsh, nme.asInstanceOf_), TypeTree(A) :: Nil)
DefDef("from".toTermName, Nil, (arg :: Nil) :: Nil, TypeTree(A), body).withFlags(Synthetic)
}

val parents = TypeTree(defn.RepresentableType.appliedTo(A)) :: Nil
New(Template(emptyConstructor, parents, EmptyValDef, repr :: to :: from :: Nil)).withPos(pos)
}

/** Assuming A is a product, synthesize a Representable instance of the following shape:
*
* ```
* new Representable[A] {
* type Repr = PCons[T1, PCons[T2, ..., PCons[Tn, PNil]]]
*
* def to(a: A): Repr =
* PCons(a._1, (PCons(a._2, ... PCons(a._n, PNil()))))
*
* def from(r: Repr): A = r match {
* case PCons(_1, Pcons(_2, ..., Pcons(_n, PNil()))) => new A(_1, _2, ..., _n)
* }
* ```
*/
def synthesizedRepresentableProduct(A: Type): untpd.Tree = {
import untpd._

val productTypes = productSelectorTypes(A)
val productTypesSize = productTypes.size
val noBounds = TypeBoundsTree(EmptyTree, EmptyTree)

val repr = TypeDef("Repr".toTypeName,
productTypes.foldRight[Tree](TypeTree(defn.PNilType)) { case (cur, acc) =>
AppliedTypeTree(TypeTree(defn.PConsType), TypeTree(cur) :: acc :: Nil)
}
)
val pNil = Apply(tpd.ref(defn.PNilModule), Nil)

val to = {
val arg = makeSyntheticParameter(tpt = TypeTree(A))
val body = (1 to productTypesSize).foldRight(pNil) { case (i, acc) =>
Apply(tpd.ref(defn.PConsModule), Select(Ident(arg.name), nme.productAccessorName(i)) :: acc :: Nil)
}
DefDef("to".toTermName, Nil, (arg :: Nil) :: Nil, Ident(repr.name), body).withFlags(Synthetic)
}

val from = {
val arg = makeSyntheticParameter(tpt = Ident(repr.name))
val pat = (1 to productTypesSize).reverse.foldLeft(pNil) { case (acc, i) =>
Apply(tpd.ref(defn.PConsModule), Ident(nme.productAccessorName(i)) :: acc :: Nil)
}
val neuu = {
A match {
case tpe: NamedType if tpe.termSymbol.exists => // case object
ref(tpe)
case _ if A.classSymbol.exists => // case class
val newArgs = (1 to productTypesSize).map(i => Ident(nme.productAccessorName(i))).toList
val hasVarargs = A.classSymbol.primaryConstructor.info.isVarArgsMethod
val newArgs0 = if (hasVarargs) newArgs.init :+ repeated(newArgs.last) else newArgs
New(TypeTree(A), newArgs0 :: Nil)
}
}
val body = Match(Ident(arg.name), CaseDef(pat, EmptyTree, neuu) :: Nil)
DefDef("from".toTermName, Nil, (arg :: Nil) :: Nil, TypeTree(A), body).withFlags(Synthetic)
}

val parents = TypeTree(defn.RepresentableType.appliedTo(A)) :: Nil
New(Template(emptyConstructor, parents, EmptyValDef, repr :: to :: from :: Nil)).withPos(pos)
}

def hasEq(tp: Type): Boolean =
inferImplicit(defn.EqType.appliedTo(tp, tp), EmptyTree, pos).isSuccess

Expand Down Expand Up @@ -645,6 +805,8 @@ trait Implicits { self: Typer =>
synthesizedClassTag(formalValue).orElse(tree)
else if (formalValue.isRef(defn.EqClass))
synthesizedEq(formalValue).orElse(tree)
else if (formalValue.isRef(defn.RepresentableClass))
synthesizedRepresentable(formalValue).orElse(tree)
else
tree
}
Expand Down
4 changes: 4 additions & 0 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1476,6 +1476,10 @@ class Typer extends Namer with TypeAssigner with Applications with Implicits wit

cls.registerTree(cdef1)

// Adds Child annotations to sealed classes
if (!cdef1.symbol.denot.isRefinementClass)
cdef1.symbol.registerIfChild()

cdef1

// todo later: check that
Expand Down
4 changes: 2 additions & 2 deletions compiler/test/dotty/tools/vulpix/ParallelTesting.scala
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,14 @@ trait ParallelTesting extends RunnerOrchestration { self =>

def runClassPath: String = outDir.getAbsolutePath + ":" + flags.runClassPath

def title: String = self match {
def title: String = (self match {
case self: JointCompilationSource =>
if (self.files.length > 1) name
else self.files.head.getPath

case self: SeparateCompilationSource =>
self.dir.getPath
}
}).stripPrefix("../")

/** Adds the flags specified in `newFlags0` if they do not already exist */
def withFlags(newFlags0: String*) = {
Expand Down
12 changes: 12 additions & 0 deletions library/src/dotty/generic/Prod.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package dotty.generic

/** Represents a heterogeneous collection: a datatype capable of storing data
* of different types. Generalises Tuples to arbitrary arity.
*/
sealed trait Prod

/** Non-empty product */
final case class PCons[H, T <: Prod](head: H, tail: T) extends Prod

/** Empty product */
final case class PNil() extends Prod
21 changes: 21 additions & 0 deletions library/src/dotty/generic/Representable.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package dotty.generic

/** A generic representation of `A` as a sum of products. Provides back and
* forth conversions between values of type `A` and values of type `Repr`.
* Instances of `Representable` for sealed traits and case classes are
* automatically synthesized by the compiler when requested implicitly.
*/
trait Representable[A] {
/** Type of the generic representation of `A`, composed of `Sum` and `Prod` types. */
type Repr // <: Sum | Prod ← when we stop cross compiling

/** Converts a value of type `A` to it's generic representation */
def to(a: A): Repr

/** Converts a value in the generic representation of `A` to its concrete representation. */
def from(r: Repr): A
}

object Representable {
def apply[A](implicit r: Representable[A]): r.type = r
}
18 changes: 18 additions & 0 deletions library/src/dotty/generic/Sum.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package dotty.generic

/** Represents a value of several possible types. An instance of Sum is either
* an instance of SLeft or of SRight. Generalises Either to arbitrary arity.
*/
sealed trait Sum

/** Non-empty sum */
sealed trait SCons[H, T <: Sum] extends Sum

/** Left case of a sum */
final case class SLeft[H, T <: Sum](head: H) extends SCons[H, T]

/** Right case of a sum */
final case class SRight[H, T <: Sum](tail: T) extends SCons[H, T]

/** Empty sum */
sealed trait SNil extends Sum
33 changes: 33 additions & 0 deletions tests/run/generic-product.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import dotty.generic._

case class Foo(i: Int, s: String)
case class Bar()

object Test {
def main(args: Array[String]): Unit = {
testFoo()
testBar()
}

def testFoo(): Unit = {
// Representable can be synthesised via implicit search
val g = Representable[Foo]

// Type is inferred correctly
val a: Representable[Foo] { type Repr = PCons[Int, PCons[String, PNil]] } = g

// Representable#to and Representable#from behave as expected:
assert(g.from(g.to(Foo(1, "s"))) == Foo(1, "s"))
}

def testBar(): Unit = {
// Representable can be synthesised via implicit search
val g = Representable[Bar]

// Type is inferred correctly
val a: Representable[Bar] { type Repr = PNil } = g

// Representable#to and Representable#from behave as expected:
assert(g.from(g.to(Bar())) == Bar())
}
}
Loading