From 18c6f97fa0671dabf5703022ae18f04daa787c7d Mon Sep 17 00:00:00 2001 From: Aggelos Biboudis Date: Thu, 12 Dec 2019 17:24:32 +0100 Subject: [PATCH 01/10] Add basic structure for refined selectable macros Co-authored-by: Nicolas Stucki --- .../ReflectionCompilerInterface.scala | 9 ++- .../dotty/tools/dotc/transform/Erasure.scala | 2 +- .../tasty/reflect/CompilerInterface.scala | 2 + .../tasty/reflect/ExtractorsPrinter.scala | 2 +- library/src/scala/tasty/reflect/TreeOps.scala | 3 +- tests/run-macros/structural-macro.check | 1 + .../run-macros/structural-macro/Macro_1.scala | 68 +++++++++++++++++++ .../run-macros/structural-macro/Test_2.scala | 21 ++++++ 8 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 tests/run-macros/structural-macro.check create mode 100644 tests/run-macros/structural-macro/Macro_1.scala create mode 100644 tests/run-macros/structural-macro/Test_2.scala diff --git a/compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala b/compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala index 51a040aec127..3e259139cd6d 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 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..56658150d01b 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 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/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/tests/run-macros/structural-macro.check b/tests/run-macros/structural-macro.check new file mode 100644 index 000000000000..f965e49cea69 --- /dev/null +++ b/tests/run-macros/structural-macro.check @@ -0,0 +1 @@ +((name,Emma),(age,42)) \ No newline at end of file diff --git a/tests/run-macros/structural-macro/Macro_1.scala b/tests/run-macros/structural-macro/Macro_1.scala new file mode 100644 index 000000000000..3dfb22286e35 --- /dev/null +++ b/tests/run-macros/structural-macro/Macro_1.scala @@ -0,0 +1,68 @@ +import scala.quoted._ + +object Macro { + + case class Record(elems: (String, Any)*) extends Selectable { + def selectDynamic(name: String): Any = elems.find(_._1 == name).get._2 + } + + inline def toHMap(s: Selectable) <: Tuple = ${ toHMapImpl('s)} + + def toHMapImpl(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: 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 list = println(rec(repr)) + + val ret = rec(repr).reverse.map(e => tupleElem(e._1, e._2)) + + Expr.ofTuple(ret) + } + + inline def toSelectable[T](s: Tuple)<: T = ${ toSelectableImpl('s, '[T])} + + def toSelectableImpl[T](s: Expr[Tuple], tpe: Type[T])(given qctx:QuoteContext): Expr[T] = { + import qctx.tasty.{given, _} + + val repr = s.unseal.tpe.widenTermRefExpr.dealias + + println(repr.show) + println(repr.showExtractors) + + // new Record((res2._1._1, res2._1._2), (res2._2._1, res2._2._2)).asInstanceOf[Record {val name: String; val age: Int} ] + + def rec(tpe: Type): List[(String, Type)] = { + tpe match { + // todo: check number based on prefix + case AppliedType(_, args) => args.map{ + case AppliedType(_, ConstantType(Constant(name: String)) :: (info: Type) :: Nil) => (name, info) + } + } + } + val r = rec(repr) + println(r) + + println(tpe.unseal.symbol) + println(TypeIdent(tpe.unseal.symbol)) + + println('{new Record()}.unseal.showExtractors) + + '{ ??? } + } +} diff --git a/tests/run-macros/structural-macro/Test_2.scala b/tests/run-macros/structural-macro/Test_2.scala new file mode 100644 index 000000000000..298c77865050 --- /dev/null +++ b/tests/run-macros/structural-macro/Test_2.scala @@ -0,0 +1,21 @@ +import Macro._ + +object Test { + + type Person = Record { + val name: String + val age: Int + } + + def main(args: Array[String]): Unit = { + val person: Person = Record("name" -> "Emma", "age" -> 42).asInstanceOf[Person] + + val res: (("name", String), ("age", Int)) = toHMap(person) + + println(res) + + // val res2: Person = toSelectable[Record](res) + + // new Record((res2._1._1, res2._1._2), (res2._2._1, res2._2._2)).asInstanceOf[Record {val name: String; val age: Int} ] + } +} From e2998b95e4d0b7f5cb5c0306e0164e80e727e484 Mon Sep 17 00:00:00 2001 From: Aggelos Biboudis Date: Fri, 13 Dec 2019 14:55:54 +0100 Subject: [PATCH 02/10] Fix checkfile --- tests/run-macros/tasty-extractors-3.check | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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"))) From b44002c10c13d4956c5c1c56d22c676ab9bdeb49 Mon Sep 17 00:00:00 2001 From: Aggelos Biboudis Date: Fri, 13 Dec 2019 14:57:15 +0100 Subject: [PATCH 03/10] Add apply method for object Refinement in tasty reflect --- .../dotc/tastyreflect/ReflectionCompilerInterface.scala | 7 +++++++ library/src/scala/tasty/reflect/CompilerInterface.scala | 2 ++ library/src/scala/tasty/reflect/TypeOrBoundsOps.scala | 5 ++++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala b/compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala index 3e259139cd6d..c9fa90768663 100644 --- a/compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala +++ b/compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala @@ -1265,6 +1265,13 @@ 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 = { + // fixme: support type refinements (type vs term) + // examine info maybe (TypeOrBounds are used for refinements) + // check aliasing of refinement + Types.RefinedType(parent, name.toTermName, 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 diff --git a/library/src/scala/tasty/reflect/CompilerInterface.scala b/library/src/scala/tasty/reflect/CompilerInterface.scala index 56658150d01b..24ba09e4763b 100644 --- a/library/src/scala/tasty/reflect/CompilerInterface.scala +++ b/library/src/scala/tasty/reflect/CompilerInterface.scala @@ -908,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 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)) From 8b17ed0144e1f1c1a0c3790b36948a4f5c2b9648 Mon Sep 17 00:00:00 2001 From: Aggelos Biboudis Date: Fri, 13 Dec 2019 14:57:44 +0100 Subject: [PATCH 04/10] Fix toSelectableImpl --- tests/run-macros/structural-macro.check | 4 +- .../run-macros/structural-macro/Macro_1.scala | 37 ++++++++++--------- .../run-macros/structural-macro/Test_2.scala | 5 ++- 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/tests/run-macros/structural-macro.check b/tests/run-macros/structural-macro.check index f965e49cea69..53ed2ed41695 100644 --- a/tests/run-macros/structural-macro.check +++ b/tests/run-macros/structural-macro.check @@ -1 +1,3 @@ -((name,Emma),(age,42)) \ No newline at end of file +((name,Emma),(age,42)) + +Record(ArraySeq((name,Emma), (age,42))) diff --git a/tests/run-macros/structural-macro/Macro_1.scala b/tests/run-macros/structural-macro/Macro_1.scala index 3dfb22286e35..ecf6c61420f5 100644 --- a/tests/run-macros/structural-macro/Macro_1.scala +++ b/tests/run-macros/structural-macro/Macro_1.scala @@ -1,9 +1,16 @@ import scala.quoted._ object Macro { - case class Record(elems: (String, Any)*) extends Selectable { def selectDynamic(name: String): Any = elems.find(_._1 == name).get._2 + def toTuple = { + //todo: unnecessary transformation? + Tuple.fromArray(elems.toArray) + } + } + + object Record { + def fromTuple(t: Tuple): Record = Record(t.toArray.toSeq.map(e => e.asInstanceOf[(String, Any)]): _*) } inline def toHMap(s: Selectable) <: Tuple = ${ toHMapImpl('s)} @@ -28,41 +35,37 @@ object Macro { } } - // val list = println(rec(repr)) - val ret = rec(repr).reverse.map(e => tupleElem(e._1, e._2)) Expr.ofTuple(ret) } - inline def toSelectable[T](s: Tuple)<: T = ${ toSelectableImpl('s, '[T])} + // note: T is not used ATM + inline def toSelectable[T](s: Tuple) <: Any = ${ toSelectableImpl('s, '[T])} - def toSelectableImpl[T](s: Expr[Tuple], tpe: Type[T])(given qctx:QuoteContext): Expr[T] = { + def toSelectableImpl[T](s: Expr[Tuple], tpe: Type[T])(given qctx:QuoteContext): Expr[Any] = { import qctx.tasty.{given, _} val repr = s.unseal.tpe.widenTermRefExpr.dealias - println(repr.show) - println(repr.showExtractors) - - // new Record((res2._1._1, res2._1._2), (res2._2._1, res2._2._2)).asInstanceOf[Record {val name: String; val age: Int} ] - def rec(tpe: Type): List[(String, Type)] = { tpe match { - // todo: check number based on prefix + // todo: + // check number based on prefix + // capture the TupleXX variants in recursion case AppliedType(_, args) => args.map{ case AppliedType(_, ConstantType(Constant(name: String)) :: (info: Type) :: Nil) => (name, info) } } } - val r = rec(repr) - println(r) - println(tpe.unseal.symbol) - println(TypeIdent(tpe.unseal.symbol)) + val r = rec(repr) - println('{new Record()}.unseal.showExtractors) + val refinementType = r.foldLeft('[Record].unseal.tpe)((acc, e) => Refinement(acc, e._1, e._2)).seal - '{ ??? } + refinementType match { + case '[$qType] => + '{ Record.fromTuple(${s}).asInstanceOf[${qType}] } + } } } diff --git a/tests/run-macros/structural-macro/Test_2.scala b/tests/run-macros/structural-macro/Test_2.scala index 298c77865050..755e6b1dceb5 100644 --- a/tests/run-macros/structural-macro/Test_2.scala +++ b/tests/run-macros/structural-macro/Test_2.scala @@ -13,9 +13,10 @@ object Test { val res: (("name", String), ("age", Int)) = toHMap(person) println(res) + println() - // val res2: Person = toSelectable[Record](res) + val res2: Person = toSelectable(res) - // new Record((res2._1._1, res2._1._2), (res2._2._1, res2._2._2)).asInstanceOf[Record {val name: String; val age: Int} ] + println(res2) } } From 3b7129781bf69296ecef24c44383cdc3072b8def Mon Sep 17 00:00:00 2001 From: Aggelos Biboudis Date: Fri, 13 Dec 2019 15:04:24 +0100 Subject: [PATCH 05/10] Rename directory and HMap to toTuple --- .../Macro_1.scala | 4 ++-- .../Test_2.scala | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename tests/run-macros/{structural-macro => refined-selectable-macro}/Macro_1.scala (92%) rename tests/run-macros/{structural-macro => refined-selectable-macro}/Test_2.scala (83%) diff --git a/tests/run-macros/structural-macro/Macro_1.scala b/tests/run-macros/refined-selectable-macro/Macro_1.scala similarity index 92% rename from tests/run-macros/structural-macro/Macro_1.scala rename to tests/run-macros/refined-selectable-macro/Macro_1.scala index ecf6c61420f5..3b7d0ea677ae 100644 --- a/tests/run-macros/structural-macro/Macro_1.scala +++ b/tests/run-macros/refined-selectable-macro/Macro_1.scala @@ -13,9 +13,9 @@ object Macro { def fromTuple(t: Tuple): Record = Record(t.toArray.toSeq.map(e => e.asInstanceOf[(String, Any)]): _*) } - inline def toHMap(s: Selectable) <: Tuple = ${ toHMapImpl('s)} + inline def toTuple(s: Selectable) <: Tuple = ${ toTupleImpl('s)} - def toHMapImpl(s: Expr[Selectable])(given qctx:QuoteContext): Expr[Tuple] = { + def toTupleImpl(s: Expr[Selectable])(given qctx:QuoteContext): Expr[Tuple] = { import qctx.tasty.{given, _} val repr = s.unseal.tpe.widenTermRefExpr.dealias diff --git a/tests/run-macros/structural-macro/Test_2.scala b/tests/run-macros/refined-selectable-macro/Test_2.scala similarity index 83% rename from tests/run-macros/structural-macro/Test_2.scala rename to tests/run-macros/refined-selectable-macro/Test_2.scala index 755e6b1dceb5..675f63df3e6c 100644 --- a/tests/run-macros/structural-macro/Test_2.scala +++ b/tests/run-macros/refined-selectable-macro/Test_2.scala @@ -10,7 +10,7 @@ object Test { def main(args: Array[String]): Unit = { val person: Person = Record("name" -> "Emma", "age" -> 42).asInstanceOf[Person] - val res: (("name", String), ("age", Int)) = toHMap(person) + val res: (("name", String), ("age", Int)) = toTuple(person) println(res) println() From 7992f6f14535f5a6911f33c47d25101d189b32df Mon Sep 17 00:00:00 2001 From: Aggelos Biboudis Date: Fri, 13 Dec 2019 18:11:20 +0100 Subject: [PATCH 06/10] Check all tuple structures Co-authored-by: Nicolas Stucki --- .../ReflectionCompilerInterface.scala | 1 + .../tasty/reflect/CompilerInterface.scala | 1 + .../tasty/reflect/StandardDefinitions.scala | 4 + .../run-macros/refined-selectable-macro.check | 5 ++ .../refined-selectable-macro/Macro_1.scala | 86 ++++++++++++------- .../refined-selectable-macro/Test_2.scala | 52 ++++++++++- tests/run-macros/structural-macro.check | 3 - 7 files changed, 116 insertions(+), 36 deletions(-) create mode 100644 tests/run-macros/refined-selectable-macro.check delete mode 100644 tests/run-macros/structural-macro.check diff --git a/compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala b/compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala index c9fa90768663..09ce6a1cc8f7 100644 --- a/compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala +++ b/compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala @@ -1870,6 +1870,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 diff --git a/library/src/scala/tasty/reflect/CompilerInterface.scala b/library/src/scala/tasty/reflect/CompilerInterface.scala index 24ba09e4763b..ea99f9f26aa9 100644 --- a/library/src/scala/tasty/reflect/CompilerInterface.scala +++ b/library/src/scala/tasty/reflect/CompilerInterface.scala @@ -1398,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/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/tests/run-macros/refined-selectable-macro.check b/tests/run-macros/refined-selectable-macro.check new file mode 100644 index 000000000000..4c5e6d397ab0 --- /dev/null +++ b/tests/run-macros/refined-selectable-macro.check @@ -0,0 +1,5 @@ +((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) diff --git a/tests/run-macros/refined-selectable-macro/Macro_1.scala b/tests/run-macros/refined-selectable-macro/Macro_1.scala index 3b7d0ea677ae..0f9e3ad73adb 100644 --- a/tests/run-macros/refined-selectable-macro/Macro_1.scala +++ b/tests/run-macros/refined-selectable-macro/Macro_1.scala @@ -1,37 +1,42 @@ import scala.quoted._ object Macro { - case class Record(elems: (String, Any)*) extends Selectable { - def selectDynamic(name: String): Any = elems.find(_._1 == name).get._2 - def toTuple = { - //todo: unnecessary transformation? - Tuple.fromArray(elems.toArray) - } - } - object Record { - def fromTuple(t: Tuple): Record = Record(t.toArray.toSeq.map(e => e.asInstanceOf[(String, Any)]): _*) + trait SelectableRecord extends Selectable { + inline def toTuple <: Tuple = ${ toTupleImpl('this)} } - inline def toTuple(s: Selectable) <: Tuple = ${ toTupleImpl('s)} + 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: _*) } ) } + } - def toTupleImpl(s: Expr[Selectable])(given qctx:QuoteContext): Expr[Tuple] = { + 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: Type) => (name, info) :: rec(parent) + 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]})) + info.seal match { case '[$qType] => + Expr.ofTuple(Seq(nameExpr, '{ $s.selectDynamic($nameExpr).asInstanceOf[$qType] })) } } @@ -40,32 +45,51 @@ object Macro { Expr.ofTuple(ret) } - // note: T is not used ATM - inline def toSelectable[T](s: Tuple) <: Any = ${ toSelectableImpl('s, '[T])} - - def toSelectableImpl[T](s: Expr[Tuple], tpe: Type[T])(given qctx:QuoteContext): Expr[Any] = { + 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 rec(tpe: Type): List[(String, Type)] = { + def isTupleCons(sym: Symbol): Boolean = sym.owner == defn.ScalaPackageClass && sym.name == "*:" + + def extractTuple(tpe: TypeOrBounds, seen: Set[String]): (Set[String], (String, Type)) = { tpe match { - // todo: - // check number based on prefix - // capture the TupleXX variants in recursion - case AppliedType(_, args) => args.map{ - case AppliedType(_, ConstantType(Constant(name: String)) :: (info: Type) :: Nil) => (name, info) - } + // 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) + val r = rec(repr, Set.empty) - val refinementType = r.foldLeft('[Record].unseal.tpe)((acc, e) => Refinement(acc, e._1, e._2)).seal + val refinementType = r.foldLeft('[T].unseal.tpe)((acc, e) => Refinement(acc, e._1, e._2)).seal - refinementType match { - case '[$qType] => - '{ Record.fromTuple(${s}).asInstanceOf[${qType}] } + refinementType match { case '[$qType] => + '{ $newRecord($s.toArray.map(e => e.asInstanceOf[(String, Any)])).asInstanceOf[${qType}] } } } } diff --git a/tests/run-macros/refined-selectable-macro/Test_2.scala b/tests/run-macros/refined-selectable-macro/Test_2.scala index 675f63df3e6c..fc1790240117 100644 --- a/tests/run-macros/refined-selectable-macro/Test_2.scala +++ b/tests/run-macros/refined-selectable-macro/Test_2.scala @@ -2,6 +2,18 @@ import Macro._ object Test { + // 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] { + def fromUntypedTuple(elems: (String, Any)*): Record = Record(elems: _*) + } + type Person = Record { val name: String val age: Int @@ -10,13 +22,49 @@ object Test { def main(args: Array[String]): Unit = { val person: Person = Record("name" -> "Emma", "age" -> 42).asInstanceOf[Person] - val res: (("name", String), ("age", Int)) = toTuple(person) + 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() - val res2: Person = toSelectable(res) + res: (("name", String), ("age", Int)) + + val res2 = Record.fromTuple(res) + + val emptyTuple = () + println(Record.fromTuple(emptyTuple)) + + // println(Record.fromTuple((1, 2))) // error + + 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(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))) println(res2) + + // Record.fromTuple[(("name", String), ("name", Int))]("name" -> "aa", "name" -> 3) // error + + res2: Record { + val name: String + val age: Int + } + + res2: Record { + val age: Int + val name: String + } } } diff --git a/tests/run-macros/structural-macro.check b/tests/run-macros/structural-macro.check deleted file mode 100644 index 53ed2ed41695..000000000000 --- a/tests/run-macros/structural-macro.check +++ /dev/null @@ -1,3 +0,0 @@ -((name,Emma),(age,42)) - -Record(ArraySeq((name,Emma), (age,42))) From da2f05b9d87bc075b05f56cfa9ee44518db9be76 Mon Sep 17 00:00:00 2001 From: Aggelos Biboudis Date: Sun, 15 Dec 2019 15:49:58 +0100 Subject: [PATCH 07/10] Apply suggestions from code review Co-Authored-By: Nicolas Stucki --- .../dotc/tastyreflect/ReflectionCompilerInterface.scala | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala b/compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala index 09ce6a1cc8f7..799faed7ddb2 100644 --- a/compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala +++ b/compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala @@ -1269,7 +1269,11 @@ class ReflectionCompilerInterface(val rootContext: core.Contexts.Context) extend // fixme: support type refinements (type vs term) // examine info maybe (TypeOrBounds are used for refinements) // check aliasing of refinement - Types.RefinedType(parent, name.toTermName, info) + 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 @@ -2003,4 +2007,3 @@ class ReflectionCompilerInterface(val rootContext: core.Contexts.Context) extend private def compilerId: Int = rootContext.outersIterator.toList.last.hashCode() } - From da393a41a30c7200c0aeef1cd904341f4b74cca0 Mon Sep 17 00:00:00 2001 From: Aggelos Biboudis Date: Mon, 16 Dec 2019 08:45:21 +0100 Subject: [PATCH 08/10] Fix indentation --- .../dotc/tastyreflect/ReflectionCompilerInterface.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala b/compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala index 799faed7ddb2..59f02e5dca59 100644 --- a/compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala +++ b/compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala @@ -1269,10 +1269,10 @@ class ReflectionCompilerInterface(val rootContext: core.Contexts.Context) extend // fixme: support type refinements (type vs term) // examine info maybe (TypeOrBounds are used for refinements) // check aliasing of refinement - val name1 = - info match - case _: TypeBounds => name.toTypeName - case _ => name.toTermName + val name1 = + info match + case _: TypeBounds => name.toTypeName + case _ => name.toTermName Types.RefinedType(parent, name1, info) } From b8075f7e1d338e5230f53496de29091caf02ff0b Mon Sep 17 00:00:00 2001 From: Aggelos Biboudis Date: Mon, 16 Dec 2019 15:05:26 +0100 Subject: [PATCH 09/10] Add a small aliased test --- .../dotc/tastyreflect/ReflectionCompilerInterface.scala | 3 --- tests/run-macros/refined-selectable-macro/Test_2.scala | 6 ++++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala b/compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala index 59f02e5dca59..baffe51a3be7 100644 --- a/compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala +++ b/compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala @@ -1266,9 +1266,6 @@ class ReflectionCompilerInterface(val rootContext: core.Contexts.Context) extend } def Refinement_apply(parent: Type, name: String, info: TypeOrBounds /* Type | TypeBounds */)(given ctx: Context): Refinement = { - // fixme: support type refinements (type vs term) - // examine info maybe (TypeOrBounds are used for refinements) - // check aliasing of refinement val name1 = info match case _: TypeBounds => name.toTypeName diff --git a/tests/run-macros/refined-selectable-macro/Test_2.scala b/tests/run-macros/refined-selectable-macro/Test_2.scala index fc1790240117..bb0a148b9fa5 100644 --- a/tests/run-macros/refined-selectable-macro/Test_2.scala +++ b/tests/run-macros/refined-selectable-macro/Test_2.scala @@ -19,6 +19,8 @@ object Test { val age: Int } + type Person2 = Person + def main(args: Array[String]): Unit = { val person: Person = Record("name" -> "Emma", "age" -> 42).asInstanceOf[Person] @@ -66,5 +68,9 @@ object Test { val age: Int val name: String } + + val p3: Person2 = person + + p3.toTuple : (("name", String), ("age", Int)) } } From b0027c61406e519fca802ead218545b7948a9765 Mon Sep 17 00:00:00 2001 From: Aggelos Biboudis Date: Tue, 17 Dec 2019 17:51:14 +0100 Subject: [PATCH 10/10] Add negtests and apply as macro to remove the need for asInstanceOf --- .../run-macros/refined-selectable-macro.check | 3 ++ .../refined-selectable-macro/Macro_2.scala | 25 +++++++++++++++ .../{Test_2.scala => Test_3.scala} | 31 ++++++++----------- 3 files changed, 41 insertions(+), 18 deletions(-) create mode 100644 tests/run-macros/refined-selectable-macro/Macro_2.scala rename tests/run-macros/refined-selectable-macro/{Test_2.scala => Test_3.scala} (57%) diff --git a/tests/run-macros/refined-selectable-macro.check b/tests/run-macros/refined-selectable-macro.check index 4c5e6d397ab0..18aec232d6d8 100644 --- a/tests/run-macros/refined-selectable-macro.check +++ b/tests/run-macros/refined-selectable-macro.check @@ -3,3 +3,6 @@ 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_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_2.scala b/tests/run-macros/refined-selectable-macro/Test_3.scala similarity index 57% rename from tests/run-macros/refined-selectable-macro/Test_2.scala rename to tests/run-macros/refined-selectable-macro/Test_3.scala index bb0a148b9fa5..fca330ec112e 100644 --- a/tests/run-macros/refined-selectable-macro/Test_2.scala +++ b/tests/run-macros/refined-selectable-macro/Test_3.scala @@ -1,18 +1,9 @@ import Macro._ +import Macro2._ -object Test { - - // 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(", ", ", ")") - } +import scala.compiletime.testing._ - object Record extends SelectableRecordCompanion[Record] { - def fromUntypedTuple(elems: (String, Any)*): Record = Record(elems: _*) - } +object Test { type Person = Record { val name: String @@ -22,7 +13,7 @@ object Test { type Person2 = Person def main(args: Array[String]): Unit = { - val person: Person = Record("name" -> "Emma", "age" -> 42).asInstanceOf[Person] + val person: Person = Record[Person]("name" -> "Emma", "age" -> 42) val res = person.toTuple @@ -49,16 +40,11 @@ object Test { val emptyTuple = () println(Record.fromTuple(emptyTuple)) - // println(Record.fromTuple((1, 2))) // error - 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(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))) println(res2) - // Record.fromTuple[(("name", String), ("name", Int))]("name" -> "aa", "name" -> 3) // error - res2: Record { val name: String val age: Int @@ -72,5 +58,14 @@ object Test { 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) + + } }