diff --git a/compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala b/compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala index 51a040aec127..baffe51a3be7 100644 --- a/compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala +++ b/compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala @@ -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 { @@ -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 @@ -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 @@ -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 @@ -1988,4 +2004,3 @@ class ReflectionCompilerInterface(val rootContext: core.Contexts.Context) extend private def compilerId: Int = rootContext.outersIterator.toList.last.hashCode() } - diff --git a/compiler/src/dotty/tools/dotc/transform/Erasure.scala b/compiler/src/dotty/tools/dotc/transform/Erasure.scala index 5f72f9214584..f2701acd60e2 100644 --- a/compiler/src/dotty/tools/dotc/transform/Erasure.scala +++ b/compiler/src/dotty/tools/dotc/transform/Erasure.scala @@ -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) => diff --git a/library/src/scala/tasty/reflect/CompilerInterface.scala b/library/src/scala/tasty/reflect/CompilerInterface.scala index 0a7c637b885a..ea99f9f26aa9 100644 --- a/library/src/scala/tasty/reflect/CompilerInterface.scala +++ b/library/src/scala/tasty/reflect/CompilerInterface.scala @@ -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 @@ -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 @@ -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 diff --git a/library/src/scala/tasty/reflect/ExtractorsPrinter.scala b/library/src/scala/tasty/reflect/ExtractorsPrinter.scala index d49eb0bf65ce..f7bbacece6ac 100644 --- a/library/src/scala/tasty/reflect/ExtractorsPrinter.scala +++ b/library/src/scala/tasty/reflect/ExtractorsPrinter.scala @@ -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) => diff --git a/library/src/scala/tasty/reflect/StandardDefinitions.scala b/library/src/scala/tasty/reflect/StandardDefinitions.scala index 7a3303b1acc8..d184d94fc4c8 100644 --- a/library/src/scala/tasty/reflect/StandardDefinitions.scala +++ b/library/src/scala/tasty/reflect/StandardDefinitions.scala @@ -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 diff --git a/library/src/scala/tasty/reflect/TreeOps.scala b/library/src/scala/tasty/reflect/TreeOps.scala index 4fc7f3af9859..c4fb30fad025 100644 --- a/library/src/scala/tasty/reflect/TreeOps.scala +++ b/library/src/scala/tasty/reflect/TreeOps.scala @@ -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) diff --git a/library/src/scala/tasty/reflect/TypeOrBoundsOps.scala b/library/src/scala/tasty/reflect/TypeOrBoundsOps.scala index e8a063bf25d8..e2a97e06e168 100644 --- a/library/src/scala/tasty/reflect/TypeOrBoundsOps.scala +++ b/library/src/scala/tasty/reflect/TypeOrBoundsOps.scala @@ -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)) } @@ -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)) diff --git a/tests/run-macros/refined-selectable-macro.check b/tests/run-macros/refined-selectable-macro.check new file mode 100644 index 000000000000..18aec232d6d8 --- /dev/null +++ b/tests/run-macros/refined-selectable-macro.check @@ -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) diff --git a/tests/run-macros/refined-selectable-macro/Macro_1.scala b/tests/run-macros/refined-selectable-macro/Macro_1.scala new file mode 100644 index 000000000000..0f9e3ad73adb --- /dev/null +++ b/tests/run-macros/refined-selectable-macro/Macro_1.scala @@ -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, ("", 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}] } + } + } +} diff --git a/tests/run-macros/refined-selectable-macro/Macro_2.scala b/tests/run-macros/refined-selectable-macro/Macro_2.scala new file mode 100644 index 000000000000..533208230d29 --- /dev/null +++ b/tests/run-macros/refined-selectable-macro/Macro_2.scala @@ -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: _*) + } +} \ No newline at end of file diff --git a/tests/run-macros/refined-selectable-macro/Test_3.scala b/tests/run-macros/refined-selectable-macro/Test_3.scala new file mode 100644 index 000000000000..fca330ec112e --- /dev/null +++ b/tests/run-macros/refined-selectable-macro/Test_3.scala @@ -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) + + + } +} diff --git a/tests/run-macros/tasty-extractors-3.check b/tests/run-macros/tasty-extractors-3.check index 57721c9234c2..f267c05d3a7d 100644 --- a/tests/run-macros/tasty-extractors-3.check +++ b/tests/run-macros/tasty-extractors-3.check @@ -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") @@ -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")))