Skip to content

Commit

Permalink
Parse arbitrary tuples construction
Browse files Browse the repository at this point in the history
  • Loading branch information
Primetalk committed May 5, 2024
1 parent c577120 commit 2c2fc94
Show file tree
Hide file tree
Showing 2 changed files with 190 additions and 17 deletions.
122 changes: 122 additions & 0 deletions quill-sql/src/main/scala/io/getquill/parser/Parser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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(_))
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
85 changes: 68 additions & 17 deletions quill-sql/src/test/scala/io/getquill/ArbitraryTupleSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
}
}

0 comments on commit 2c2fc94

Please sign in to comment.