Skip to content

Commit d935be6

Browse files
Merge pull request #7731 from dotty-staging/refined-selectable
Add refined tupled records prototype
2 parents b26a4ac + b0027c6 commit d935be6

File tree

12 files changed

+235
-7
lines changed

12 files changed

+235
-7
lines changed

compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,11 @@ class ReflectionCompilerInterface(val rootContext: core.Contexts.Context) extend
272272
case _ => term
273273
}
274274

275+
def TypeRef_apply(sym: Symbol)(given Context): TypeTree = {
276+
assert(sym.isType)
277+
withDefaultPos(tpd.ref(sym).asInstanceOf[tpd.TypeTree])
278+
}
279+
275280
type Ref = tpd.RefTree
276281

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

284-
def Ref_apply(sym: Symbol)(given Context): Ref =
289+
def Ref_apply(sym: Symbol)(given Context): Ref = {
290+
assert(sym.isTerm)
285291
withDefaultPos(tpd.ref(sym).asInstanceOf[tpd.RefTree])
292+
}
286293

287294
type Ident = tpd.Ident
288295

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

1268+
def Refinement_apply(parent: Type, name: String, info: TypeOrBounds /* Type | TypeBounds */)(given ctx: Context): Refinement = {
1269+
val name1 =
1270+
info match
1271+
case _: TypeBounds => name.toTypeName
1272+
case _ => name.toTermName
1273+
Types.RefinedType(parent, name1, info)
1274+
}
1275+
12611276
def Refinement_parent(self: Refinement)(given Context): Type = self.parent
12621277
def Refinement_name(self: Refinement)(given Context): String = self.refinedName.toString
12631278
def Refinement_info(self: Refinement)(given Context): TypeOrBounds = self.refinedInfo
@@ -1856,6 +1871,7 @@ class ReflectionCompilerInterface(val rootContext: core.Contexts.Context) extend
18561871
def Definitions_FunctionClass(arity: Int, isImplicit: Boolean, isErased: Boolean): Symbol =
18571872
defn.FunctionClass(arity, isImplicit, isErased).asClass
18581873
def Definitions_TupleClass(arity: Int): Symbol = defn.TupleType(arity).classSymbol.asClass
1874+
def Definitions_isTupleClass(sym: Symbol): Boolean = defn.isTupleClass(sym)
18591875

18601876
def Definitions_InternalQuoted_patternHole: Symbol = defn.InternalQuoted_patternHole
18611877
def Definitions_InternalQuoted_patternBindHoleAnnot: Symbol = defn.InternalQuoted_patternBindHoleAnnot

compiler/src/dotty/tools/dotc/transform/Erasure.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,7 @@ object Erasure {
523523
}
524524

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

528528
ntree match {
529529
case TypeApply(fun, args) =>

library/src/scala/tasty/reflect/CompilerInterface.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,8 @@ trait CompilerInterface {
559559

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

562+
def TypeRef_apply(sym: Symbol)(given ctx: Context): TypeTree
563+
562564
/** Type tree representing a reference to definition with a given name */
563565
type TypeIdent <: TypeTree
564566

@@ -906,6 +908,8 @@ trait CompilerInterface {
906908

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

911+
def Refinement_apply(parent: Type, name: String, info: TypeOrBounds /* Type | TypeBounds */)(given ctx: Context): Refinement
912+
909913
def Refinement_parent(self: Refinement)(given ctx: Context): Type
910914
def Refinement_name(self: Refinement)(given ctx: Context): String
911915
def Refinement_info(self: Refinement)(given ctx: Context): TypeOrBounds
@@ -1394,6 +1398,7 @@ trait CompilerInterface {
13941398
def Definitions_FunctionClass(arity: Int, isImplicit: Boolean = false, isErased: Boolean = false): Symbol
13951399

13961400
def Definitions_TupleClass(arity: Int): Symbol
1401+
def Definitions_isTupleClass(sym: Symbol): Boolean
13971402

13981403
/** Symbol of scala.internal.Quoted.patternHole */
13991404
def Definitions_InternalQuoted_patternHole: Symbol

library/src/scala/tasty/reflect/ExtractorsPrinter.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ class ExtractorsPrinter[R <: Reflection & Singleton](val tasty: R) extends Print
190190
case TypeRef(qual, name) =>
191191
this += "TypeRef(" += qual += ", \"" += name += "\")"
192192
case Refinement(parent, name, info) =>
193-
this += "Refinement(" += parent += ", " += name += ", " += info += ")"
193+
this += "Refinement(" += parent += ", \"" += name += "\", " += info += ")"
194194
case AppliedType(tycon, args) =>
195195
this += "AppliedType(" += tycon += ", " ++= args += ")"
196196
case AnnotatedType(underlying, annot) =>

library/src/scala/tasty/reflect/StandardDefinitions.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,10 @@ trait StandardDefinitions extends Core {
144144
def TupleClass(arity: Int): Symbol =
145145
internal.Definitions_TupleClass(arity)
146146

147+
/** Returns `true` if `sym` is a `Tuple1`, `Tuple2`, ... `Tuple22` */
148+
def isTupleClass(sym: Symbol): Boolean =
149+
internal.Definitions_isTupleClass(sym)
150+
147151
/** Contains Scala primitive value classes:
148152
* - Byte
149153
* - Short

library/src/scala/tasty/reflect/TreeOps.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -892,7 +892,8 @@ trait TreeOps extends Core {
892892
def unapply(x: TypeIdent): Some[TypeIdent] = Some(x)
893893

894894
object TypeIdent {
895-
// TODO def apply(name: String)(given ctx: Context): TypeIdent
895+
def apply(sym: Symbol)(given ctx: Context): TypeTree =
896+
internal.TypeRef_apply(sym)
896897
def copy(original: Tree)(name: String)(given ctx: Context): TypeIdent =
897898
internal.TypeIdent_copy(original)(name)
898899
def unapply(x: TypeIdent)(given ctx: Context): Option[String] = Some(x.name)

library/src/scala/tasty/reflect/TypeOrBoundsOps.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,9 @@ trait TypeOrBoundsOps extends Core {
167167
def unapply(x: Refinement)(given ctx: Context): Option[Refinement] = Some(x)
168168

169169
object Refinement {
170+
def apply(parent: Type, name: String, info: TypeOrBounds /* Type | TypeBounds */)(given ctx: Context): Refinement =
171+
internal.Refinement_apply(parent, name, info)
172+
170173
def unapply(x: Refinement)(given ctx: Context): Option[(Type, String, TypeOrBounds /* Type | TypeBounds */)] =
171174
Some((x.parent, x.name, x.info))
172175
}
@@ -184,7 +187,7 @@ trait TypeOrBoundsOps extends Core {
184187
def unapply(x: AppliedType)(given ctx: Context): Option[AppliedType] = Some(x)
185188

186189
object AppliedType {
187-
def apply(tycon: Type, args: List[TypeOrBounds])(given ctx: Context) : AppliedType =
190+
def apply(tycon: Type, args: List[TypeOrBounds])(given ctx: Context): AppliedType =
188191
internal.AppliedType_apply(tycon, args)
189192
def unapply(x: AppliedType)(given ctx: Context): Option[(Type, List[TypeOrBounds /* Type | TypeBounds */])] =
190193
Some((x.tycon, x.args))
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
((name,Emma),(age,42))
2+
3+
Record()
4+
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)
5+
Record(name=Emma, age=42)
6+
Error(Tuple type was not explicit expected `(S, T)` where S is a singleton string,Record.fromTuple((1, 2)),17,Typer)
7+
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)
8+
Error(Repeated record name: name,Record.fromTuple[(("name", String), ("name", Int))]("name" -> "aa", "name" -> 3),52,Typer)
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import scala.quoted._
2+
3+
object Macro {
4+
5+
trait SelectableRecord extends Selectable {
6+
inline def toTuple <: Tuple = ${ toTupleImpl('this)}
7+
}
8+
9+
trait SelectableRecordCompanion[T] {
10+
protected def fromUntypedTuple(elems: (String, Any)*): T
11+
inline def fromTuple[T <: Tuple](s: T) <: Any = ${ fromTupleImpl('s, '{ (x: Array[(String, Any)]) => fromUntypedTuple(x: _*) } ) }
12+
}
13+
14+
private def toTupleImpl(s: Expr[Selectable])(given qctx:QuoteContext): Expr[Tuple] = {
15+
import qctx.tasty.{given, _}
16+
17+
val repr = s.unseal.tpe.widenTermRefExpr.dealias
18+
19+
def rec(tpe: Type): List[(String, Type)] = {
20+
tpe match {
21+
case Refinement(parent, name, info) =>
22+
info match {
23+
case _: TypeBounds =>
24+
rec(parent)
25+
case _: MethodType | _: PolyType | _: TypeBounds =>
26+
qctx.warning(s"Ignored $name as a field of the record", s)
27+
rec(parent)
28+
case info: Type =>
29+
(name, info) :: rec(parent)
30+
}
31+
32+
case _ => Nil
33+
}
34+
}
35+
36+
def tupleElem(name: String, info: Type): Expr[Any] = {
37+
val nameExpr = Expr(name)
38+
info.seal match { case '[$qType] =>
39+
Expr.ofTuple(Seq(nameExpr, '{ $s.selectDynamic($nameExpr).asInstanceOf[$qType] }))
40+
}
41+
}
42+
43+
val ret = rec(repr).reverse.map(e => tupleElem(e._1, e._2))
44+
45+
Expr.ofTuple(ret)
46+
}
47+
48+
private def fromTupleImpl[T: Type](s: Expr[Tuple], newRecord: Expr[Array[(String, Any)] => T])(given qctx:QuoteContext): Expr[Any] = {
49+
import qctx.tasty.{given, _}
50+
51+
val repr = s.unseal.tpe.widenTermRefExpr.dealias
52+
53+
def isTupleCons(sym: Symbol): Boolean = sym.owner == defn.ScalaPackageClass && sym.name == "*:"
54+
55+
def extractTuple(tpe: TypeOrBounds, seen: Set[String]): (Set[String], (String, Type)) = {
56+
tpe match {
57+
// Tuple2(S, T) where S must be a constant string type
58+
case AppliedType(parent, ConstantType(Constant(name: String)) :: (info: Type) :: Nil) if (parent.typeSymbol == defn.TupleClass(2)) =>
59+
if seen(name) then
60+
qctx.error(s"Repeated record name: $name", s)
61+
(seen + name, (name, info))
62+
case _ =>
63+
qctx.error("Tuple type was not explicit expected `(S, T)` where S is a singleton string", s)
64+
(seen, ("<error>", defn.AnyType))
65+
}
66+
}
67+
def rec(tpe: Type, seen: Set[String]): List[(String, Type)] = {
68+
if tpe =:= defn.UnitType then Nil
69+
else tpe match {
70+
// head *: tail
71+
case AppliedType(parent, List(head, tail: Type)) if isTupleCons(parent.typeSymbol) =>
72+
val (seen2, head2) = extractTuple(head, seen)
73+
head2 :: rec(tail, seen2)
74+
// Tuple1(...), Tuple2(...), ..., Tuple22(...)
75+
case AppliedType(parent, args) if defn.isTupleClass(parent.typeSymbol) =>
76+
(args.foldLeft((seen, List.empty[(String, Type)])){ case ((seenAcc, acc), arg) =>
77+
val (seen3, arg2) = extractTuple(arg, seenAcc)
78+
(seen3, arg2 :: acc)
79+
})._2
80+
// Tuple
81+
case _ =>
82+
qctx.error("Tuple type must be of known size", s)
83+
Nil
84+
}
85+
}
86+
87+
val r = rec(repr, Set.empty)
88+
89+
val refinementType = r.foldLeft('[T].unseal.tpe)((acc, e) => Refinement(acc, e._1, e._2)).seal
90+
91+
refinementType match { case '[$qType] =>
92+
'{ $newRecord($s.toArray.map(e => e.asInstanceOf[(String, Any)])).asInstanceOf[${qType}] }
93+
}
94+
}
95+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
2+
import scala.quoted._
3+
import Macro._
4+
5+
object Macro2 {
6+
// TODO should elems of `new Record` and `Record.fromUntypedTuple` be IArray[Object]
7+
// This would make it possible to keep the same reference to the elements when transforming a Tuple into a Record (or vice versa)
8+
9+
case class Record(elems: (String, Any)*) extends SelectableRecord {
10+
def selectDynamic(name: String): Any = elems.find(_._1 == name).get._2
11+
override def toString(): String = elems.map(x => x._1 + "=" + x._2).mkString("Record(", ", ", ")")
12+
}
13+
14+
object Record extends SelectableRecordCompanion[Record] {
15+
import scala.quoted._
16+
17+
inline def apply[R <: Record](elems: (String, Any)*) : R = ${ applyImpl('elems, '[R]) }
18+
19+
def applyImpl[R <: Record: Type](elems: Expr[Seq[(String, Any)]], ev: Type[R])(given qctx: QuoteContext) = {
20+
'{ new Record($elems:_*).asInstanceOf[$ev] }
21+
}
22+
23+
def fromUntypedTuple(elems: (String, Any)*): Record = Record(elems: _*)
24+
}
25+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import Macro._
2+
import Macro2._
3+
4+
import scala.compiletime.testing._
5+
6+
object Test {
7+
8+
type Person = Record {
9+
val name: String
10+
val age: Int
11+
}
12+
13+
type Person2 = Person
14+
15+
def main(args: Array[String]): Unit = {
16+
val person: Person = Record[Person]("name" -> "Emma", "age" -> 42)
17+
18+
val res = person.toTuple
19+
20+
val p0 = person.asInstanceOf[Record {
21+
val name: String
22+
def age: Int // ignored
23+
}]
24+
p0.toTuple
25+
26+
val p2: Record {
27+
val age: Int
28+
val name: String
29+
} = person
30+
31+
p2.toTuple : (("age", Int), ("name", String))
32+
33+
println(res)
34+
println()
35+
36+
res: (("name", String), ("age", Int))
37+
38+
val res2 = Record.fromTuple(res)
39+
40+
val emptyTuple = ()
41+
println(Record.fromTuple(emptyTuple))
42+
43+
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)
44+
println(Record.fromTuple(xxl))
45+
46+
println(res2)
47+
48+
res2: Record {
49+
val name: String
50+
val age: Int
51+
}
52+
53+
res2: Record {
54+
val age: Int
55+
val name: String
56+
}
57+
58+
val p3: Person2 = person
59+
60+
p3.toTuple : (("name", String), ("age", Int))
61+
62+
// Neg-tests
63+
println(typeCheckErrors("Record.fromTuple((1, 2))").head)
64+
65+
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)
66+
67+
typeCheckErrors("Record.fromTuple[((\"name\", String), (\"name\", Int))](\"name\" -> \"aa\", \"name\" -> 3)").foreach(println)
68+
69+
70+
}
71+
}

tests/run-macros/tasty-extractors-3.check

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ TypeRef(ThisType(TypeRef(NoPrefix(), "scala")), "Nothing")
3030

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

33-
Refinement(TypeRef(NoPrefix(), "Foo"), X, TypeBounds(TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "scala")), "Predef"), "String"), TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "scala")), "Predef"), "String")))
33+
Refinement(TypeRef(NoPrefix(), "Foo"), "X", TypeBounds(TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "scala")), "Predef"), "String"), TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "scala")), "Predef"), "String")))
3434

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

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

4141
TypeRef(NoPrefix(), "$anon")
4242

43-
Refinement(TypeRef(NoPrefix(), "Foo"), X, TypeBounds(TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "scala")), "Predef"), "String"), TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "scala")), "Predef"), "String")))
43+
Refinement(TypeRef(NoPrefix(), "Foo"), "X", TypeBounds(TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "scala")), "Predef"), "String"), TypeRef(TermRef(ThisType(TypeRef(NoPrefix(), "scala")), "Predef"), "String")))
4444

0 commit comments

Comments
 (0)