Skip to content

Commit 7992f6f

Browse files
Check all tuple structures
Co-authored-by: Nicolas Stucki <[email protected]>
1 parent 3b71297 commit 7992f6f

File tree

7 files changed

+116
-36
lines changed

7 files changed

+116
-36
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1870,6 +1870,7 @@ class ReflectionCompilerInterface(val rootContext: core.Contexts.Context) extend
18701870
def Definitions_FunctionClass(arity: Int, isImplicit: Boolean, isErased: Boolean): Symbol =
18711871
defn.FunctionClass(arity, isImplicit, isErased).asClass
18721872
def Definitions_TupleClass(arity: Int): Symbol = defn.TupleType(arity).classSymbol.asClass
1873+
def Definitions_isTupleClass(sym: Symbol): Boolean = defn.isTupleClass(sym)
18731874

18741875
def Definitions_InternalQuoted_patternHole: Symbol = defn.InternalQuoted_patternHole
18751876
def Definitions_InternalQuoted_patternBindHoleAnnot: Symbol = defn.InternalQuoted_patternBindHoleAnnot

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1398,6 +1398,7 @@ trait CompilerInterface {
13981398
def Definitions_FunctionClass(arity: Int, isImplicit: Boolean = false, isErased: Boolean = false): Symbol
13991399

14001400
def Definitions_TupleClass(arity: Int): Symbol
1401+
def Definitions_isTupleClass(sym: Symbol): Boolean
14011402

14021403
/** Symbol of scala.internal.Quoted.patternHole */
14031404
def Definitions_InternalQuoted_patternHole: Symbol

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
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
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)
Lines changed: 55 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,42 @@
11
import scala.quoted._
22

33
object Macro {
4-
case class Record(elems: (String, Any)*) extends Selectable {
5-
def selectDynamic(name: String): Any = elems.find(_._1 == name).get._2
6-
def toTuple = {
7-
//todo: unnecessary transformation?
8-
Tuple.fromArray(elems.toArray)
9-
}
10-
}
114

12-
object Record {
13-
def fromTuple(t: Tuple): Record = Record(t.toArray.toSeq.map(e => e.asInstanceOf[(String, Any)]): _*)
5+
trait SelectableRecord extends Selectable {
6+
inline def toTuple <: Tuple = ${ toTupleImpl('this)}
147
}
158

16-
inline def toTuple(s: Selectable) <: Tuple = ${ toTupleImpl('s)}
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+
}
1713

18-
def toTupleImpl(s: Expr[Selectable])(given qctx:QuoteContext): Expr[Tuple] = {
14+
private def toTupleImpl(s: Expr[Selectable])(given qctx:QuoteContext): Expr[Tuple] = {
1915
import qctx.tasty.{given, _}
2016

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

2319
def rec(tpe: Type): List[(String, Type)] = {
2420
tpe match {
25-
case Refinement(parent, name, info: Type) => (name, info) :: rec(parent)
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+
2632
case _ => Nil
2733
}
2834
}
2935

3036
def tupleElem(name: String, info: Type): Expr[Any] = {
3137
val nameExpr = Expr(name)
32-
info.seal match {
33-
case '[$qType] =>
34-
Expr.ofTuple(Seq(nameExpr, '{$s.selectDynamic($nameExpr).asInstanceOf[$qType]}))
38+
info.seal match { case '[$qType] =>
39+
Expr.ofTuple(Seq(nameExpr, '{ $s.selectDynamic($nameExpr).asInstanceOf[$qType] }))
3540
}
3641
}
3742

@@ -40,32 +45,51 @@ object Macro {
4045
Expr.ofTuple(ret)
4146
}
4247

43-
// note: T is not used ATM
44-
inline def toSelectable[T](s: Tuple) <: Any = ${ toSelectableImpl('s, '[T])}
45-
46-
def toSelectableImpl[T](s: Expr[Tuple], tpe: Type[T])(given qctx:QuoteContext): Expr[Any] = {
48+
private def fromTupleImpl[T: Type](s: Expr[Tuple], newRecord: Expr[Array[(String, Any)] => T])(given qctx:QuoteContext): Expr[Any] = {
4749
import qctx.tasty.{given, _}
4850

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

51-
def rec(tpe: Type): List[(String, Type)] = {
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)) = {
5256
tpe match {
53-
// todo:
54-
// check number based on prefix
55-
// capture the TupleXX variants in recursion
56-
case AppliedType(_, args) => args.map{
57-
case AppliedType(_, ConstantType(Constant(name: String)) :: (info: Type) :: Nil) => (name, info)
58-
}
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
5984
}
6085
}
6186

62-
val r = rec(repr)
87+
val r = rec(repr, Set.empty)
6388

64-
val refinementType = r.foldLeft('[Record].unseal.tpe)((acc, e) => Refinement(acc, e._1, e._2)).seal
89+
val refinementType = r.foldLeft('[T].unseal.tpe)((acc, e) => Refinement(acc, e._1, e._2)).seal
6590

66-
refinementType match {
67-
case '[$qType] =>
68-
'{ Record.fromTuple(${s}).asInstanceOf[${qType}] }
91+
refinementType match { case '[$qType] =>
92+
'{ $newRecord($s.toArray.map(e => e.asInstanceOf[(String, Any)])).asInstanceOf[${qType}] }
6993
}
7094
}
7195
}

tests/run-macros/refined-selectable-macro/Test_2.scala

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@ import Macro._
22

33
object Test {
44

5+
// TODO should elems of `new Record` and `Record.fromUntypedTuple` be IArray[Object]
6+
// This would make it possible to keep the same reference to the elements when transforming a Tuple into a Record (or vice versa)
7+
8+
case class Record(elems: (String, Any)*) extends SelectableRecord {
9+
def selectDynamic(name: String): Any = elems.find(_._1 == name).get._2
10+
override def toString(): String = elems.map(x => x._1 + "=" + x._2).mkString("Record(", ", ", ")")
11+
}
12+
13+
object Record extends SelectableRecordCompanion[Record] {
14+
def fromUntypedTuple(elems: (String, Any)*): Record = Record(elems: _*)
15+
}
16+
517
type Person = Record {
618
val name: String
719
val age: Int
@@ -10,13 +22,49 @@ object Test {
1022
def main(args: Array[String]): Unit = {
1123
val person: Person = Record("name" -> "Emma", "age" -> 42).asInstanceOf[Person]
1224

13-
val res: (("name", String), ("age", Int)) = toTuple(person)
25+
val res = person.toTuple
26+
27+
val p0 = person.asInstanceOf[Record {
28+
val name: String
29+
def age: Int // ignored
30+
}]
31+
p0.toTuple
32+
33+
val p2: Record {
34+
val age: Int
35+
val name: String
36+
} = person
37+
38+
p2.toTuple : (("age", Int), ("name", String))
1439

1540
println(res)
1641
println()
1742

18-
val res2: Person = toSelectable(res)
43+
res: (("name", String), ("age", Int))
44+
45+
val res2 = Record.fromTuple(res)
46+
47+
val emptyTuple = ()
48+
println(Record.fromTuple(emptyTuple))
49+
50+
// println(Record.fromTuple((1, 2))) // error
51+
52+
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)
53+
println(Record.fromTuple(xxl))
54+
// println(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)))
1955

2056
println(res2)
57+
58+
// Record.fromTuple[(("name", String), ("name", Int))]("name" -> "aa", "name" -> 3) // error
59+
60+
res2: Record {
61+
val name: String
62+
val age: Int
63+
}
64+
65+
res2: Record {
66+
val age: Int
67+
val name: String
68+
}
2169
}
2270
}

tests/run-macros/structural-macro.check

Lines changed: 0 additions & 3 deletions
This file was deleted.

0 commit comments

Comments
 (0)