Skip to content

Commit

Permalink
Implement static operator for splicing constants e.g. static(Constant…
Browse files Browse the repository at this point in the history
…s.Foo) (#16)
  • Loading branch information
deusaquilus authored Aug 17, 2021
1 parent 1b2870b commit 8345399
Show file tree
Hide file tree
Showing 13 changed files with 589 additions and 42 deletions.
3 changes: 3 additions & 0 deletions quill-sql/src/main/scala/io/getquill/Dsl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import io.getquill.context.UnquoteMacro
import io.getquill.context.LiftMacro
import io.getquill._
import io.getquill.dsl.InfixDsl
import io.getquill.context.StaticSpliceMacro

// trait Quoter {
// def quote[T](bodyExpr: Quoted[T]): Quoted[T] = ???
Expand Down Expand Up @@ -77,6 +78,8 @@ trait QueryDsl {
trait QuoteDsl {
import scala.language.implicitConversions

inline def static[T](inline value: T): T = ${ StaticSpliceMacro('value) }

inline def insertMeta[T](inline exclude: (T => Any)*): InsertMeta[T] = ${ InsertMetaMacro[T]('exclude) }

inline def updateMeta[T](inline exclude: (T => Any)*): UpdateMeta[T] = ${ UpdateMetaMacro[T]('exclude) }
Expand Down
1 change: 1 addition & 0 deletions quill-sql/src/main/scala/io/getquill/GetQuill.scala
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
package io.getquill

export Dsl._
export StaticSplice._
72 changes: 72 additions & 0 deletions quill-sql/src/main/scala/io/getquill/StaticSplice.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package io.getquill

import scala.quoted._
import java.time.format.DateTimeFormatter
import io.getquill.util.Format
import io.getquill.util.LoadModule
import scala.util.Try
import scala.util.Failure
import scala.util.Success
import io.getquill.util.CommonExtensions.Either._

/**
* Trait that allows usage of 'static' block. Can declared one of these and use similar to encoders
* but it needs to be compiled in a previous compilation unit and a global static.
* TODO More explanation
*/
trait StaticSplice[T]:
def apply(value: T): String

object StaticSplice:
import io.getquill.metaprog.Extractors._

object Summon:
def apply[T: Type](using Quotes): Either[String, StaticSplice[T]] =
import quotes.reflect.{ Try => TTry, _}
for {
summonValue <- Expr.summon[StaticSplice[T]].toEitherOr(s"a StaticSplice[${Format.TypeOf[T]}] cannot be summoned")
// Summoning StaticSplice[T] will given (SpliceString: StaticSplice[String])
// (a.k.a. Typed(Ident(SpliceString), TypeTree(StaticSplice[String])) usually with an outer inline surrounding it all)
// so then we need to use Untype to just get SpliceString which is a module that we can load
staticSpliceType = Untype(summonValue.asTerm.underlyingArgument).tpe.widen

untypedModule <- LoadModule.TypeRepr(staticSpliceType).toEither.mapLeft(_.getMessage)
module <- Try(untypedModule.asInstanceOf[StaticSplice[T]]).toEither.mapLeft(_.getMessage)
} yield (module)

object SpliceString extends StaticSplice[String]:
def apply(value: String) = s"'${value}'"
inline given StaticSplice[String] = SpliceString

object SpliceInt extends StaticSplice[Int]:
def apply(value: Int) = s"${value}"
inline given StaticSplice[Int] = SpliceInt

object SpliceShort extends StaticSplice[Short]:
def apply(value: Short) = s"${value}"
inline given StaticSplice[Short] = SpliceShort

object SpliceLong extends StaticSplice[Long]:
def apply(value: Long) = s"${value}"
inline given StaticSplice[Long] = SpliceLong

object SpliceFloat extends StaticSplice[Float]:
def apply(value: Float) = s"${value}"
inline given StaticSplice[Float] = SpliceFloat

object SpliceDouble extends StaticSplice[Double]:
def apply(value: Double) = s"${value}"
inline given StaticSplice[Double] = SpliceDouble

object SpliceDate extends StaticSplice[java.sql.Date]:
def apply(value: java.sql.Date) =
value.toLocalDate.format(DateTimeFormatter.ofPattern("yyyyMMdd"))
inline given StaticSplice[java.sql.Date] = SpliceDate

object SpliceLocalDate extends StaticSplice[java.time.LocalDate]:
def apply(value: java.time.LocalDate) =
value.format(DateTimeFormatter.ofPattern("yyyyMMdd"))
inline given StaticSplice[java.time.LocalDate] = SpliceLocalDate


end StaticSplice
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package io.getquill.context

import scala.quoted._
import io.getquill.StaticSplice
import io.getquill.util.LoadModule
import io.getquill.metaprog.Extractors
import scala.util.Success
import scala.util.Failure
import scala.util.Try
import scala.util.Either
import io.getquill.util.Format

private[getquill] object ReflectivePathChainLookup:
sealed trait LookupElement { def cls: Class[_]; def current: Object }
object LookupElement:
// For a module class the lookup-object is actualy a class. For example
// for: object Foo { object Bar { ... } } you would do:
// val submod: Class[Bar] = Class[Foo].getDeclaredClasses.find(_.name endsWith "Bar$")
// submod.getField("MODULE$").get(submod /*Object is passed into here*/)
// (note that the Class[---] things are typed above but in reality when dealing with them in the reflection they won't be)
case class ModuleClass(cls: Class[_]) extends LookupElement { val current: Object = cls }
// For a regular value reference, we can simply lookup the class from the object
case class Value(current: Object) extends LookupElement { val cls = current.getClass }
end LookupElement

case class LookupPath(element: LookupElement, path: String):
def cls = element.cls
def current = element.current

extension (elem: Option[Object])
def nullCheck(path: String, cls: Class[_], lookupType: String) =
elem match
case Some(null) =>
println(s"The ${lookupType} ${path} can be looked up from ${cls} but the value is null")
None
case other =>
other

object Lookup:



// If it's a method on a regular (i.e. dynamic) class. Try to that up
object Method:
def unapply(lookup: LookupPath): Option[LookupElement.Value] =
lookupFirstMethod(lookup.path)(lookup.cls, lookup.current)("method").map(LookupElement.Value(_))

// if it's a field on a regular (i.e. dynamic) class. Try to look that up
object Field:
def unapply(lookup: LookupPath): Option[LookupElement.Value] =
lookupFirstMethod(lookup.path)(lookup.cls, lookup.current)("field").map(LookupElement.Value(_))

// Scala object-in-object e.g. object Foo { object Bar { ... } }. Lookup the `Bar`
// it will be represented in java as: path.to.Foo$Bar.
object Submodule:
def unapply(lookup: LookupPath): Option[LookupElement.ModuleClass] =
val submod = lookup.cls.getDeclaredClasses.find(c => c.getName.endsWith(lookup.path + "$"))
submod.orElse {
// Odd pattern for top level object: object Foo { object Bar }
// there won't be a Bar in getDeclaredClasses but instead a Bar field on Foo whose value is null
lookup.cls.getFields.find(_.getName == lookup.path).map(_.getType)
}.map(LookupElement.ModuleClass(_))

object HelperObjectField:
def unapply(lookup: LookupPath): Option[LookupElement.Value] =
// Get Foo.MODULE$
val submodOpt: Option[Object] = lookupModuleObject(lookup.current)(lookup.cls)
// Get Foo.MODULE$.fields. The `Field` unapply can be recycled for this purpose
submodOpt.map(submod =>
// I.e. lookup MODULE$.field
lookupFirstMethod(lookup.path)(lookup.cls, submod)("$MODULE.field").map(LookupElement.Value(_))
).flatten

object HelperObjectMethod:
def unapply(lookup: LookupPath): Option[LookupElement.Value] =
// Get Foo.MODULE$
val submodOpt: Option[Object] = lookupModuleObject(lookup.current)(lookup.cls)
// Get Foo.MODULE$.methods. The `Method` unapply can be recycled for this purpose
submodOpt.map(submod =>
// I.e. lookup MODULE$.method
lookupFirstMethod(lookup.path)(lookup.cls, submod)("$MODULE.method").map(LookupElement.Value(_))
).flatten

// Lookup object Foo { ... } element MODULE$ which is the singleton instance in the Java representation
def lookupModuleObject(obj: Object)(cls: Class[_] = obj.getClass): Option[Object] =
cls.getFields.find(_.getName == "MODULE$").map(m => Try(m.get(obj)).toOption).flatten

def lookupFirstMethod(path: String)(cls: Class[_], instance: Object)(label: String) =
val methodOpt = cls.getMethods.find(m => m.getName == path && m.getParameterCount == 0)
methodOpt.map(m => Try(m.invoke(instance)).toOption).flatten.nullCheck(path, cls, label)

def lookupFirstField(path: String)(cls: Class[_], instance: Object)(label: String) =
val fieldOpt = cls.getFields.find(_.getName == path)
fieldOpt.map(f => Try(f.get(instance)).toOption).flatten.nullCheck(path, cls, label)

end Lookup

import java.lang.reflect.{ Method, Field }
def singleLookup(elem: LookupElement, path: String): Option[LookupElement] =
LookupPath(elem, path) match
case Lookup.Submodule(elem) => Some(elem)
case Lookup.Method(elem) => Some(elem)
case Lookup.Field(elem) => Some(elem)
case Lookup.HelperObjectMethod(elem) => Some(elem)
case Lookup.HelperObjectField(elem) => Some(elem)
case _ => None

def chainLookup(element: LookupElement, paths: List[String])(pathsSeen: List[String] = List()): Either[String, LookupElement] =
import StringOps._
paths match
case Nil => Right(element)
case head :: tail =>
val nextElementOpt = singleLookup(element, head)
nextElementOpt match
case Some(nextElement) =>
chainLookup(nextElement, tail)(pathsSeen :+ head)
case None =>
Left(
s"Could not look up the path `${pathsSeen.dots}[$head]` from the `${element.cls.getName}` ${element.current}.\n" +
s"Remaining path: ${paths.mkString(".")}"
)

def apply(obj: Object, paths: List[String]) =
chainLookup(LookupElement.Value(obj), paths)()

object StringOps:
extension (strList: List[String])
def dots =
if (!strList.isEmpty) strList.mkString("", ".", ".") else ""

end ReflectivePathChainLookup
147 changes: 147 additions & 0 deletions quill-sql/src/main/scala/io/getquill/context/StaticSpliceMacro.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package io.getquill.context

import scala.quoted._
import io.getquill.StaticSplice
import io.getquill.util.LoadModule
import io.getquill.metaprog.Extractors
import scala.util.Success
import scala.util.Failure
import scala.util.Try
import scala.util.Right
import scala.util.Left
import io.getquill.util.Format
import io.getquill.Quoted
import io.getquill.quat.QuatMaking
import io.getquill.parser.Lifter
import scala.util.Try
import io.getquill.StaticSplice
import io.getquill.util.CommonExtensions.Either._
import io.getquill.util.CommonExtensions.Throwable._
import io.getquill.util.CommonExtensions.For._

object StaticSpliceMacro {
import Extractors._

private[getquill] object SelectPath:
def recurseInto(using Quotes)(term: quotes.reflect.Term, accum: List[String] = List()): Option[(quotes.reflect.Term, List[String])] =
import quotes.reflect._
term match
// Recurses through a series of selects do the core identifier e.g:
// Select(Select(Ident("core"), "foo"), "bar") => recurseInto( {Select(Ident("core"), "foo")}, "bar" +: List("baz") )
case IgnoreApplyNoargs(Select(inner, pathNode)) => recurseInto(inner, pathNode +: accum)
case id: Ident => Some((id, accum))
// If at the core of the nested selects is not a Ident, this does not match
case other => None

def unapply(using Quotes)(term: quotes.reflect.Term): Option[(quotes.reflect.Term, List[String])] =
import quotes.reflect._
term match
// recurse on Module.Something
case select: Select => recurseInto(select)
// recurse on Module.SomethingAply() from which the empty-args apply i.e. `()` needs to be ignored
case select @ IgnoreApplyNoargs(_:Select) => recurseInto(select)
case id: Ident => Some((id, List()))
case _ => None
end SelectPath

extension [T](opt: Option[T])
def nullAsNone =
opt match
case Some(null) => None
case _ => opt

object DefTerm:
def unapply(using Quotes)(term: quotes.reflect.Term): Option[quotes.reflect.Term] =
import quotes.reflect._
if (term.tpe.termSymbol.isValDef || term.tpe.termSymbol.isDefDef) Some(term)
else None

def isModule(using Quotes)(sym: quotes.reflect.Symbol) =
import quotes.reflect._
val f = sym.flags
f.is(Flags.Module) && !f.is(Flags.Package) && !f.is(Flags.Param) && !f.is(Flags.ParamAccessor) && !f.is(Flags.Method)

object TermIsModule:
def unapply(using Quotes)(value: quotes.reflect.Term): Boolean =
import quotes.reflect.{Try => _, _}
val tpe = value.tpe.widen
if (isModule(tpe.typeSymbol))
true
else
false

/** The term is a static module but not a package */
object TermOwnerIsModule:
def unapply(using Quotes)(value: quotes.reflect.Term): Option[quotes.reflect.TypeRepr] =
import quotes.reflect.{Try => _, _}
Try(value.tpe.termSymbol.owner).toOption.flatMap { owner =>
val memberType = value.tpe.memberType(owner)
if (isModule(memberType.typeSymbol))
Some(memberType)
else
None
}

def apply[T: Type](valueRaw: Expr[T])(using Quotes): Expr[T] =
import quotes.reflect.{ Try => _, _ }
import ReflectivePathChainLookup.StringOps._
import io.getquill.ast._

val value = valueRaw.asTerm.underlyingArgument

// TODO summon a Expr[StaticSplicer] using the T type passed originally.
// Then use use LoadModule to get the value of that thing during runtime so we can use it
// (i.e. see io.getquill.metaprog.SummonParser on how to do that)
// for primitive types e.g. String, Int, Float etc... rather then making summonable splicers
// it is easier to just splice them directly, since otherwise those StaticSplicer modules themselves
// need to be compiled in a previous compilation unit, and we want to define them here.
// Technically that should be fine because they will only actually be used in the testing code though
// should think about this more later. For now just do toString to check that stuff from the main return works

val (pathRoot, selectPath) =
Untype(value) match
case SelectPath(pathRoot, selectPath) => (pathRoot, selectPath)
case other =>
// TODO Long explanatory message about how it has to some value inside object foo inside object bar... and it needs to be a thing compiled in a previous compilation unit
report.throwError(s"Could not load a static value `${Format.Term(value)}` from ${Printer.TreeStructure.show(other)}")


val (ownerTpe, path) =
pathRoot match
case term @ DefTerm(TermIsModule()) =>
(pathRoot.tpe, selectPath)
// TODO Maybe only check module owner if Path is Nil?
case term @ DefTerm(TermOwnerIsModule(owner)) =>
(owner, pathRoot.symbol.name +: selectPath)
case _ =>
report.throwError(s"Cannot evaluate the static path ${Format.Term(value)}. Neither it's type ${Format.TypeRepr(pathRoot.tpe)} nor the owner of this type is a static module.")

val module = LoadModule.TypeRepr(ownerTpe).toEither.discardLeft(e =>
// TODO Long explanatory message about how it has to some value inside object foo inside object bar... and it needs to be a thing compiled in a previous compilation unit
report.throwError(s"Could not look up {${(ownerTpe)}}.${path.mkString(".")} from the object.\nStatic load failed due to: ${e.stackTraceToString}")
)

val splicedValue =
ReflectivePathChainLookup(module, path).discardLeft(msg =>
report.throwError(s"Could not look up {${(ownerTpe)}}.${path.mkString(".")}. Failed because:\n${msg}")
)

val quat = Lifter.quat(QuatMaking.ofType[T])

def errorMsg(error: String) =
s"Could not statically splice ${Format.Term(value)} because ${error}"

val spliceEither =
for {
castSplice <- Try(splicedValue.current.asInstanceOf[T]).toEither.mapLeft(e => errorMsg(e.getMessage))
splicer <- StaticSplice.Summon[T].mapLeft(str => errorMsg(str))
splice <- Try(splicer(castSplice)).toEither.mapLeft(e => errorMsg(e.getMessage))
} yield splice

val spliceStr =
spliceEither match
case Left(msg) => report.throwError(msg, valueRaw)
case Right(value) => value

UnquoteMacro('{ Quoted[T](Infix(List(${Expr(spliceStr)}), List(), true, $quat),Nil, Nil) })
}
Loading

0 comments on commit 8345399

Please sign in to comment.