Skip to content

Add refined tupled records prototype #7731

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

Merged
merged 10 commits into from
Dec 20, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,11 @@ class ReflectionCompilerInterface(val rootContext: core.Contexts.Context) extend
case _ => term
}

def TypeRef_apply(sym: Symbol)(given Context): TypeTree = {
assert(sym.isType)
withDefaultPos(tpd.ref(sym).asInstanceOf[tpd.TypeTree])
}

type Ref = tpd.RefTree

def isInstanceOfRef(given ctx: Context): IsInstanceOf[Ref] = new {
Expand All @@ -281,8 +286,10 @@ class ReflectionCompilerInterface(val rootContext: core.Contexts.Context) extend
case _ => None
}

def Ref_apply(sym: Symbol)(given Context): Ref =
def Ref_apply(sym: Symbol)(given Context): Ref = {
assert(sym.isTerm)
withDefaultPos(tpd.ref(sym).asInstanceOf[tpd.RefTree])
}

type Ident = tpd.Ident

Expand Down Expand Up @@ -1258,6 +1265,14 @@ class ReflectionCompilerInterface(val rootContext: core.Contexts.Context) extend
case _ => None
}

def Refinement_apply(parent: Type, name: String, info: TypeOrBounds /* Type | TypeBounds */)(given ctx: Context): Refinement = {
val name1 =
info match
case _: TypeBounds => name.toTypeName
case _ => name.toTermName
Types.RefinedType(parent, name1, info)
}

def Refinement_parent(self: Refinement)(given Context): Type = self.parent
def Refinement_name(self: Refinement)(given Context): String = self.refinedName.toString
def Refinement_info(self: Refinement)(given Context): TypeOrBounds = self.refinedInfo
Expand Down Expand Up @@ -1856,6 +1871,7 @@ class ReflectionCompilerInterface(val rootContext: core.Contexts.Context) extend
def Definitions_FunctionClass(arity: Int, isImplicit: Boolean, isErased: Boolean): Symbol =
defn.FunctionClass(arity, isImplicit, isErased).asClass
def Definitions_TupleClass(arity: Int): Symbol = defn.TupleType(arity).classSymbol.asClass
def Definitions_isTupleClass(sym: Symbol): Boolean = defn.isTupleClass(sym)

def Definitions_InternalQuoted_patternHole: Symbol = defn.InternalQuoted_patternHole
def Definitions_InternalQuoted_patternBindHoleAnnot: Symbol = defn.InternalQuoted_patternBindHoleAnnot
Expand Down Expand Up @@ -1988,4 +2004,3 @@ class ReflectionCompilerInterface(val rootContext: core.Contexts.Context) extend

private def compilerId: Int = rootContext.outersIterator.toList.last.hashCode()
}

2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/transform/Erasure.scala
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,7 @@ object Erasure {
}

override def typedTypeApply(tree: untpd.TypeApply, pt: Type)(implicit ctx: Context): Tree = {
val ntree = interceptTypeApply(tree.asInstanceOf[TypeApply])(ctx.withPhase(ctx.erasurePhase))
val ntree = interceptTypeApply(tree.asInstanceOf[TypeApply])(ctx.withPhase(ctx.erasurePhase)).withSpan(tree.span)

ntree match {
case TypeApply(fun, args) =>
Expand Down
5 changes: 5 additions & 0 deletions library/src/scala/tasty/reflect/CompilerInterface.scala
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,8 @@ trait CompilerInterface {

def Inferred_apply(tpe: Type)(given ctx: Context): Inferred

def TypeRef_apply(sym: Symbol)(given ctx: Context): TypeTree

/** Type tree representing a reference to definition with a given name */
type TypeIdent <: TypeTree

Expand Down Expand Up @@ -906,6 +908,8 @@ trait CompilerInterface {

def isInstanceOfRefinement(given ctx: Context): IsInstanceOf[Refinement]

def Refinement_apply(parent: Type, name: String, info: TypeOrBounds /* Type | TypeBounds */)(given ctx: Context): Refinement

def Refinement_parent(self: Refinement)(given ctx: Context): Type
def Refinement_name(self: Refinement)(given ctx: Context): String
def Refinement_info(self: Refinement)(given ctx: Context): TypeOrBounds
Expand Down Expand Up @@ -1394,6 +1398,7 @@ trait CompilerInterface {
def Definitions_FunctionClass(arity: Int, isImplicit: Boolean = false, isErased: Boolean = false): Symbol

def Definitions_TupleClass(arity: Int): Symbol
def Definitions_isTupleClass(sym: Symbol): Boolean

/** Symbol of scala.internal.Quoted.patternHole */
def Definitions_InternalQuoted_patternHole: Symbol
Expand Down
2 changes: 1 addition & 1 deletion library/src/scala/tasty/reflect/ExtractorsPrinter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ class ExtractorsPrinter[R <: Reflection & Singleton](val tasty: R) extends Print
case TypeRef(qual, name) =>
this += "TypeRef(" += qual += ", \"" += name += "\")"
case Refinement(parent, name, info) =>
this += "Refinement(" += parent += ", " += name += ", " += info += ")"
this += "Refinement(" += parent += ", \"" += name += "\", " += info += ")"
case AppliedType(tycon, args) =>
this += "AppliedType(" += tycon += ", " ++= args += ")"
case AnnotatedType(underlying, annot) =>
Expand Down
4 changes: 4 additions & 0 deletions library/src/scala/tasty/reflect/StandardDefinitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ trait StandardDefinitions extends Core {
def TupleClass(arity: Int): Symbol =
internal.Definitions_TupleClass(arity)

/** Returns `true` if `sym` is a `Tuple1`, `Tuple2`, ... `Tuple22` */
def isTupleClass(sym: Symbol): Boolean =
internal.Definitions_isTupleClass(sym)

/** Contains Scala primitive value classes:
* - Byte
* - Short
Expand Down
3 changes: 2 additions & 1 deletion library/src/scala/tasty/reflect/TreeOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -892,7 +892,8 @@ trait TreeOps extends Core {
def unapply(x: TypeIdent): Some[TypeIdent] = Some(x)

object TypeIdent {
// TODO def apply(name: String)(given ctx: Context): TypeIdent
def apply(sym: Symbol)(given ctx: Context): TypeTree =
internal.TypeRef_apply(sym)
def copy(original: Tree)(name: String)(given ctx: Context): TypeIdent =
internal.TypeIdent_copy(original)(name)
def unapply(x: TypeIdent)(given ctx: Context): Option[String] = Some(x.name)
Expand Down
5 changes: 4 additions & 1 deletion library/src/scala/tasty/reflect/TypeOrBoundsOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ trait TypeOrBoundsOps extends Core {
def unapply(x: Refinement)(given ctx: Context): Option[Refinement] = Some(x)

object Refinement {
def apply(parent: Type, name: String, info: TypeOrBounds /* Type | TypeBounds */)(given ctx: Context): Refinement =
internal.Refinement_apply(parent, name, info)

def unapply(x: Refinement)(given ctx: Context): Option[(Type, String, TypeOrBounds /* Type | TypeBounds */)] =
Some((x.parent, x.name, x.info))
}
Expand All @@ -184,7 +187,7 @@ trait TypeOrBoundsOps extends Core {
def unapply(x: AppliedType)(given ctx: Context): Option[AppliedType] = Some(x)

object AppliedType {
def apply(tycon: Type, args: List[TypeOrBounds])(given ctx: Context) : AppliedType =
def apply(tycon: Type, args: List[TypeOrBounds])(given ctx: Context): AppliedType =
internal.AppliedType_apply(tycon, args)
def unapply(x: AppliedType)(given ctx: Context): Option[(Type, List[TypeOrBounds /* Type | TypeBounds */])] =
Some((x.tycon, x.args))
Expand Down
8 changes: 8 additions & 0 deletions tests/run-macros/refined-selectable-macro.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
((name,Emma),(age,42))

Record()
Record(field1=1, field2=2, field3=3, field4=4, field5=5, field6=6, field7=7, field8=8, field9=9, field10=10, field11=11, field12=12, field13=13, field14=14, field15=15, field16=16, field17=17, field18=18, field19=19, field20=20, field21=21, field22=22, field23=23, field24=24, field25=25)
Record(name=Emma, age=42)
Error(Tuple type was not explicit expected `(S, T)` where S is a singleton string,Record.fromTuple((1, 2)),17,Typer)
Error(Tuple type was not explicit expected `(S, T)` where S is a singleton string,Record.fromTuple(("field1" -> 1,"field2" -> 2,"field3" -> 3,"field4" -> 4,"field5" -> 5,"field6" -> 6,"field7" -> 7,"field8" -> 8,"field9" -> 9,"field10" -> 10,"field11" -> 11,"field12" -> 12,"field13" -> 13,"field14" -> 14,"field15" -> 15,"field16" -> 16,"field17" -> 17,"field18" -> 18,"field19" -> 19,"field20" -> 20,"field21" -> 21,"field22" -> 22,"field23" -> 23,"field24" -> 24,"field25" -> 25)),17,Typer)
Error(Repeated record name: name,Record.fromTuple[(("name", String), ("name", Int))]("name" -> "aa", "name" -> 3),52,Typer)
95 changes: 95 additions & 0 deletions tests/run-macros/refined-selectable-macro/Macro_1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import scala.quoted._

object Macro {

trait SelectableRecord extends Selectable {
inline def toTuple <: Tuple = ${ toTupleImpl('this)}
}

trait SelectableRecordCompanion[T] {
protected def fromUntypedTuple(elems: (String, Any)*): T
inline def fromTuple[T <: Tuple](s: T) <: Any = ${ fromTupleImpl('s, '{ (x: Array[(String, Any)]) => fromUntypedTuple(x: _*) } ) }
}

private def toTupleImpl(s: Expr[Selectable])(given qctx:QuoteContext): Expr[Tuple] = {
import qctx.tasty.{given, _}

val repr = s.unseal.tpe.widenTermRefExpr.dealias

def rec(tpe: Type): List[(String, Type)] = {
tpe match {
case Refinement(parent, name, info) =>
info match {
case _: TypeBounds =>
rec(parent)
case _: MethodType | _: PolyType | _: TypeBounds =>
qctx.warning(s"Ignored $name as a field of the record", s)
rec(parent)
case info: Type =>
(name, info) :: rec(parent)
}

case _ => Nil
}
}

def tupleElem(name: String, info: Type): Expr[Any] = {
val nameExpr = Expr(name)
info.seal match { case '[$qType] =>
Expr.ofTuple(Seq(nameExpr, '{ $s.selectDynamic($nameExpr).asInstanceOf[$qType] }))
}
}

val ret = rec(repr).reverse.map(e => tupleElem(e._1, e._2))

Expr.ofTuple(ret)
}

private def fromTupleImpl[T: Type](s: Expr[Tuple], newRecord: Expr[Array[(String, Any)] => T])(given qctx:QuoteContext): Expr[Any] = {
import qctx.tasty.{given, _}

val repr = s.unseal.tpe.widenTermRefExpr.dealias

def isTupleCons(sym: Symbol): Boolean = sym.owner == defn.ScalaPackageClass && sym.name == "*:"

def extractTuple(tpe: TypeOrBounds, seen: Set[String]): (Set[String], (String, Type)) = {
tpe match {
// Tuple2(S, T) where S must be a constant string type
case AppliedType(parent, ConstantType(Constant(name: String)) :: (info: Type) :: Nil) if (parent.typeSymbol == defn.TupleClass(2)) =>
if seen(name) then
qctx.error(s"Repeated record name: $name", s)
(seen + name, (name, info))
case _ =>
qctx.error("Tuple type was not explicit expected `(S, T)` where S is a singleton string", s)
(seen, ("<error>", defn.AnyType))
}
}
def rec(tpe: Type, seen: Set[String]): List[(String, Type)] = {
if tpe =:= defn.UnitType then Nil
else tpe match {
// head *: tail
case AppliedType(parent, List(head, tail: Type)) if isTupleCons(parent.typeSymbol) =>
val (seen2, head2) = extractTuple(head, seen)
head2 :: rec(tail, seen2)
// Tuple1(...), Tuple2(...), ..., Tuple22(...)
case AppliedType(parent, args) if defn.isTupleClass(parent.typeSymbol) =>
(args.foldLeft((seen, List.empty[(String, Type)])){ case ((seenAcc, acc), arg) =>
val (seen3, arg2) = extractTuple(arg, seenAcc)
(seen3, arg2 :: acc)
})._2
// Tuple
case _ =>
qctx.error("Tuple type must be of known size", s)
Nil
}
}

val r = rec(repr, Set.empty)

val refinementType = r.foldLeft('[T].unseal.tpe)((acc, e) => Refinement(acc, e._1, e._2)).seal

refinementType match { case '[$qType] =>
'{ $newRecord($s.toArray.map(e => e.asInstanceOf[(String, Any)])).asInstanceOf[${qType}] }
}
}
}
25 changes: 25 additions & 0 deletions tests/run-macros/refined-selectable-macro/Macro_2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@

import scala.quoted._
import Macro._

object Macro2 {
// TODO should elems of `new Record` and `Record.fromUntypedTuple` be IArray[Object]
// This would make it possible to keep the same reference to the elements when transforming a Tuple into a Record (or vice versa)

case class Record(elems: (String, Any)*) extends SelectableRecord {
def selectDynamic(name: String): Any = elems.find(_._1 == name).get._2
override def toString(): String = elems.map(x => x._1 + "=" + x._2).mkString("Record(", ", ", ")")
}

object Record extends SelectableRecordCompanion[Record] {
import scala.quoted._

inline def apply[R <: Record](elems: (String, Any)*) : R = ${ applyImpl('elems, '[R]) }

def applyImpl[R <: Record: Type](elems: Expr[Seq[(String, Any)]], ev: Type[R])(given qctx: QuoteContext) = {
'{ new Record($elems:_*).asInstanceOf[$ev] }
}

def fromUntypedTuple(elems: (String, Any)*): Record = Record(elems: _*)
}
}
71 changes: 71 additions & 0 deletions tests/run-macros/refined-selectable-macro/Test_3.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import Macro._
import Macro2._

import scala.compiletime.testing._

object Test {

type Person = Record {
val name: String
val age: Int
}

type Person2 = Person

def main(args: Array[String]): Unit = {
val person: Person = Record[Person]("name" -> "Emma", "age" -> 42)

val res = person.toTuple

val p0 = person.asInstanceOf[Record {
val name: String
def age: Int // ignored
}]
p0.toTuple

val p2: Record {
val age: Int
val name: String
} = person

p2.toTuple : (("age", Int), ("name", String))

println(res)
println()

res: (("name", String), ("age", Int))

val res2 = Record.fromTuple(res)

val emptyTuple = ()
println(Record.fromTuple(emptyTuple))

val xxl: (("field1", Int),("field2", Int),("field3", Int),("field4", Int),("field5", Int),("field6", Int),("field7", Int),("field8", Int),("field9", Int),("field10", Int),("field11", Int),("field12", Int),("field13", Int),("field14", Int),("field15", Int),("field16", Int),("field17", Int),("field18", Int),("field19", Int),("field20", Int),("field21", Int),("field22", Int),("field23", Int),("field24", Int),("field25", Int)) = ("field1" -> 1,"field2" -> 2,"field3" -> 3,"field4" -> 4,"field5" -> 5,"field6" -> 6,"field7" -> 7,"field8" -> 8,"field9" -> 9,"field10" -> 10,"field11" -> 11,"field12" -> 12,"field13" -> 13,"field14" -> 14,"field15" -> 15,"field16" -> 16,"field17" -> 17,"field18" -> 18,"field19" -> 19,"field20" -> 20,"field21" -> 21,"field22" -> 22,"field23" -> 23,"field24" -> 24,"field25" -> 25)
println(Record.fromTuple(xxl))

println(res2)

res2: Record {
val name: String
val age: Int
}

res2: Record {
val age: Int
val name: String
}

val p3: Person2 = person

p3.toTuple : (("name", String), ("age", Int))

// Neg-tests
println(typeCheckErrors("Record.fromTuple((1, 2))").head)

println(typeCheckErrors("Record.fromTuple((\"field1\" -> 1,\"field2\" -> 2,\"field3\" -> 3,\"field4\" -> 4,\"field5\" -> 5,\"field6\" -> 6,\"field7\" -> 7,\"field8\" -> 8,\"field9\" -> 9,\"field10\" -> 10,\"field11\" -> 11,\"field12\" -> 12,\"field13\" -> 13,\"field14\" -> 14,\"field15\" -> 15,\"field16\" -> 16,\"field17\" -> 17,\"field18\" -> 18,\"field19\" -> 19,\"field20\" -> 20,\"field21\" -> 21,\"field22\" -> 22,\"field23\" -> 23,\"field24\" -> 24,\"field25\" -> 25))").head)

typeCheckErrors("Record.fromTuple[((\"name\", String), (\"name\", Int))](\"name\" -> \"aa\", \"name\" -> 3)").foreach(println)


}
}
4 changes: 2 additions & 2 deletions tests/run-macros/tasty-extractors-3.check
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ TypeRef(ThisType(TypeRef(NoPrefix(), "scala")), "Nothing")

TypeRef(ThisType(TypeRef(NoPrefix(), "scala")), "Any")

Refinement(TypeRef(NoPrefix(), "Foo"), X, TypeBounds(TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "scala")), "Predef"), "String"), TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "scala")), "Predef"), "String")))
Refinement(TypeRef(NoPrefix(), "Foo"), "X", TypeBounds(TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "scala")), "Predef"), "String"), TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "scala")), "Predef"), "String")))

TypeRef(ThisType(TypeRef(NoPrefix(), "scala")), "Unit")

Expand All @@ -40,5 +40,5 @@ TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "scala")), "Predef"), "String")

TypeRef(NoPrefix(), "$anon")

Refinement(TypeRef(NoPrefix(), "Foo"), X, TypeBounds(TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "scala")), "Predef"), "String"), TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "scala")), "Predef"), "String")))
Refinement(TypeRef(NoPrefix(), "Foo"), "X", TypeBounds(TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "scala")), "Predef"), "String"), TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "scala")), "Predef"), "String")))