From 2c2fc9454fb4e568e04892ea8ff4a21925d4063f Mon Sep 17 00:00:00 2001 From: Arseniy Zhizhelev Date: Sun, 5 May 2024 23:15:29 +0300 Subject: [PATCH] Parse arbitrary tuples construction --- .../scala/io/getquill/parser/Parser.scala | 122 ++++++++++++++++++ .../io/getquill/ArbitraryTupleSpec.scala | 85 +++++++++--- 2 files changed, 190 insertions(+), 17 deletions(-) diff --git a/quill-sql/src/main/scala/io/getquill/parser/Parser.scala b/quill-sql/src/main/scala/io/getquill/parser/Parser.scala index f120fe48..be033eb5 100644 --- a/quill-sql/src/main/scala/io/getquill/parser/Parser.scala +++ b/quill-sql/src/main/scala/io/getquill/parser/Parser.scala @@ -53,6 +53,7 @@ trait ParserLibrary extends ParserFactory { protected def functionParser(using Quotes, TranspileConfig) = ParserChain.attempt(FunctionParser(_)) protected def functionApplyParser(using Quotes, TranspileConfig) = ParserChain.attempt(FunctionApplyParser(_)) protected def valParser(using Quotes, TranspileConfig) = ParserChain.attempt(ValParser(_)) + protected def arbitraryTupleParser(using Quotes, TranspileConfig) = ParserChain.attempt(ArbitraryTupleBlockParser(_)) protected def blockParser(using Quotes, TranspileConfig) = ParserChain.attempt(BlockParser(_)) protected def extrasParser(using Quotes, TranspileConfig) = ParserChain.attempt(ExtrasParser(_)) protected def operationsParser(using Quotes, TranspileConfig) = ParserChain.attempt(OperationsParser(_)) @@ -88,6 +89,7 @@ trait ParserLibrary extends ParserFactory { .orElse(functionParser) // decided to have it be it's own parser unlike Quill3 .orElse(patMatchParser) .orElse(valParser) + .orElse(arbitraryTupleParser) .orElse(blockParser) .orElse(operationsParser) .orElse(extrasParser) @@ -153,6 +155,126 @@ class ValParser(val rootParse: Parser)(using Quotes, TranspileConfig) case Unseal(ValDefTerm(ast)) => ast } } +/** + * Matches `runtime.Tuples.cons(head,tail)`. + */ +object TupleCons { + def unapply(using Quotes)(t: quotes.reflect.Term): Option[(quotes.reflect.Term, quotes.reflect.Term)] = { + import quotes.reflect.* + t match { + case Apply(Select(Select(Ident("runtime"), "Tuples"), "cons"), List(head, tail)) => + Some((head, tail)) + case _ => + None + } + } +} + +/** + * Matches inner.asInstanceOf[T]: T + */ +object AsInstanceOf { + def unapply(using Quotes)(term: quotes.reflect.Term): Option[quotes.reflect.Term] = { + import quotes.reflect._ + term match { + case TypeApply(Select(inner, "asInstanceOf"), _) => Some(inner) + case _ => None + } + } +} + +/** + * Matches an inlined call to `Tuple.*:`: + * {{{ + * { + * val Tuple_this: scala.Tuple$package.EmptyTuple.type = scala.Tuple$package.EmptyTuple + * + * (scala.runtime.Tuples.cons(i, Tuple_this).asInstanceOf[scala.*:[scala.Int, scala.Tuple$package.EmptyTuple.type]]: scala.*:[scala.Int, scala.Tuple$package.EmptyTuple]) + * } + * }}} + */ +object ArbitraryTupleConstructionInlined { + def unapply(using Quotes)(t: quotes.reflect.Term): Option[(quotes.reflect.Term, quotes.reflect.Term)] = { + import quotes.reflect.{Ident => TIdent, *} + t match { + case + Inlined( + // Some(Apply( + // TypeApply( + // Select( + // Inlined(_,_, + // Typed(AsInstanceOf(TupleCons(a,b)), _) + // ), + // "*:" + // ), + // _ + // ), + // _ + // )), + _, + List(ValDef("Tuple_this", _, Some(prevTuple))), + Typed(AsInstanceOf(TupleCons(head, TIdent("Tuple_this"))), _) + ) => + Some((head, prevTuple)) + case _ => None + } + } +} + +/** + * Parses a few cases of arbitrary tuples. + * + * Scala 3 produces a few different trees for arbitrary tuples. Method `*:` is marked as inline. + * Under the hood it actually invokes `Tuples.cons` function: + * {{{ + * inline def *: [H, This >: this.type <: Tuple] (x: H): H *: This = + * runtime.Tuples.cons(x, this).asInstanceOf[H *: This] + * }}} + * So, at least we have to match Tuples.cons. + * However, it's not the only variation. Scala also produces a block with intermediate val `Tuple_this` definitions: + * {{{ + * { + * val Tuple_this: scala.Tuple$package.EmptyTuple.type = scala.Tuple$package.EmptyTuple + * val `Tuple_thisâ‚‚`: scala.*:[java.lang.String, scala.Tuple$package.EmptyTuple] = (scala.runtime.Tuples.cons("", Tuple_this).asInstanceOf[scala.*:[java.lang.String, scala.Tuple$package.EmptyTuple.type]]: scala.*:[java.lang.String, scala.Tuple$package.EmptyTuple]) + * + * (scala.runtime.Tuples.cons(1, `Tuple_thisâ‚‚`).asInstanceOf[scala.*:[scala.Int, scala.*:[java.lang.String, scala.Tuple$package.EmptyTuple]]]: scala.*:[scala.Int, scala.*:[java.lang.String, scala.Tuple$package.EmptyTuple]]) + * } + * }}} + */ +class ArbitraryTupleBlockParser(val rootParse: Parser)(using Quotes, TranspileConfig) + extends Parser(rootParse) + with PatternMatchingValues { + + import quotes.reflect.{Block => TBlock, Ident => TIdent, _} + + def attempt = { + case '{EmptyTuple} => + ast.Tuple(List()) + case '{$a *: EmptyTuple} => + val aAst = rootParse(a) + ast.Tuple(List(aAst)) + case inlined@Unseal(ArbitraryTupleConstructionInlined(singleValue, prevTuple)) => + //printExpr(inlined, "singleValue ArbitraryTupleConstructionInlined") + val headAst = rootParse(singleValue.asExpr) + val prevTupleAst = rootParse(prevTuple.asExpr) + prevTupleAst match { + case ast.Tuple(lst) => ast.Tuple(headAst :: lst) + case _ => + throw IllegalArgumentException(s"Unexpected tuple ast ${prevTupleAst}") + } + case block@Unseal(TBlock(parts, ArbitraryTupleConstructionInlined(head,Typed(AsInstanceOf(TupleCons(head2,TIdent("Tuple_this"))), _)) )) if (parts.length > 0) => + //println(s"Large block. parts.size=${parts.size}; parts:\n- ${parts.mkString("\n- ")}") + val headAst = rootParse(head.asExpr) + val head2Ast = rootParse(head2.asExpr) + val partsAsts = headAst :: head2Ast :: parts.reverse.flatMap{ + case ValDef("Tuple_this", tpe, Some(TIdent("EmptyTuple"))) => List() + case ValDef("Tuple_this", tpe, Some(Typed(AsInstanceOf(TupleCons(next,TIdent("Tuple_this"))), _))) => List(rootParse(next.asExpr)) + case ValDef("Tuple_this", tpe, Some(unknown)) => + throw IllegalArgumentException(s"Unexpected Tuple_this = ${unknown.show}") + } + Tuple(partsAsts) + } +} class BlockParser(val rootParse: Parser)(using Quotes, TranspileConfig) extends Parser(rootParse) diff --git a/quill-sql/src/test/scala/io/getquill/ArbitraryTupleSpec.scala b/quill-sql/src/test/scala/io/getquill/ArbitraryTupleSpec.scala index 64825321..1fba4294 100644 --- a/quill-sql/src/test/scala/io/getquill/ArbitraryTupleSpec.scala +++ b/quill-sql/src/test/scala/io/getquill/ArbitraryTupleSpec.scala @@ -11,37 +11,32 @@ class ArbitraryTupleSpec extends Spec { type MyRow1 = (Int, String) type MyRow2 = Int *: String *: EmptyTuple - "ordinary tuple" in { - inline def q = quote{ - querySchema[MyRow1]("my_table", t => t._1 -> "int_field", t => t._2 -> "string_field") - } + inline def myRow1Query = quote { + querySchema[MyRow1]("my_table", t => t._1 -> "int_field", t => t._2 -> "string_field") + } + + inline def myRow2Query = quote { + querySchema[MyRow2]("my_table", t => t._1 -> "int_field", t => t._2 -> "string_field") + } - val result = ctx.run(q) + "ordinary tuple" in { + val result = ctx.run(myRow1Query) result.extractor(Row(123, "St"), MirrorSession.default) mustEqual (123, "St") } "arbitrary long tuple" in { - inline def q = quote{ - querySchema[MyRow2]("my_table", t => t._1 -> "int_field", t => t._2 -> "string_field") - } - - val result = ctx.run(q) + val result = ctx.run(myRow2Query) result.extractor(Row(123, "St"), MirrorSession.default) mustEqual (123, "St") } "get field of arbitrary long tuple" in { - inline def q = quote{ - querySchema[MyRow2]("my_table", t => t._1 -> "int_field", t => t._2 -> "string_field") - } - inline def g = quote{ - q.map{t => t match { - case h *: tail => h - } + myRow2Query.map{ + case h *: tail => h } } val result = ctx.run(g) @@ -50,4 +45,60 @@ class ArbitraryTupleSpec extends Spec { (123) } + "decode empty tuple" in { + inline def g = quote { + myRow2Query.map{ + case (_, _) => EmptyTuple + } + } + + val result = ctx.run(g) + + result.extractor(Row(123, "St"), MirrorSession.default) mustEqual EmptyTuple + } + + "construct tuple1" in { + inline def g = quote { + myRow1Query.map { + case (i, s) => i *: EmptyTuple + } + } + + val result = ctx.run(g) + + result.extractor(Row(123, "St"), MirrorSession.default) mustEqual + Tuple1(123) + } + + "construct arbitrary tuple (incorrect)" in { + inline def g = quote { + myRow1Query.map { + case (i, s) => + s *: + i *: + EmptyTuple + } + } + val result = ctx.run(g) + + result.extractor(Row("St", 123), MirrorSession.default) mustEqual + ("St", 123) + + } + + "constant arbitrary tuple" in { + inline def g = quote { + 1 *: "" *: true *: (0.0 *: EmptyTuple) + } + val result = ctx.run(g) + result.extractor(Row(123, "St", false, 3.14), MirrorSession.default) mustEqual (123, "St", false, 3.14)//(1, "", true, 0.0) + } + + "constant arbitrary tuple 1" in { + inline def g = quote { + (0.0 *: EmptyTuple) + } + val result = ctx.run(g) + result.extractor(Row(3.14), MirrorSession.default) mustEqual Tuple1(3.14) + } }