Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

Improved custom builder lookup (solves #6) #7

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.softwaremill.macmemo

import scala.language.experimental.macros

object BuilderResolver {

def resolve(methodFullName: String): MemoCacheBuilder = macro builderResolverMacro_impl

def builderResolverMacro_impl(c: scala.reflect.macros.whitebox.Context)(methodFullName: c.Expr[String]): c.Expr[MemoCacheBuilder] = {
import c.universe._

def bringDefaultBuilder: Tree = {
val Literal(Constant(mfn: String)) = methodFullName.tree
val msg = s"Cannot find custom memo builder for '$mfn' - default builder will be used"
c.info(c.enclosingPosition, msg, false)
reify {
MemoCacheBuilder.guavaMemoCacheBuilder
}.tree
}

val builderTree = c.inferImplicitValue(typeOf[MemoCacheBuilder]) match {
case EmptyTree => bringDefaultBuilder
case foundBuilderTree => foundBuilderTree
}

c.Expr[MemoCacheBuilder](Block(List(), builderTree))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it seems that putting an auxiliary macro call inside a quasiquote makes c.inferImplicitValue() work well, where calling this from an annotation macro context doesn't work in all cases? That's really meta-meta :) But it looks kinda like a bug to me, or at least some weird incosistency I cannot understand...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to do some investigation about this behaviour.
My Current clues:

  • I can't use auxilary macro directly, I need to weave a call to it in expanded code, because both macros live in the same compilation unit.
  • annotation macro can't infer implicits vals from enclosing class scope (implicit val members), but we are able to traverse them manually. This means that problem does not lie in lack of enclosure members visibility. Maybe this behaviour is intended? From @ macros we not only can add new members but even whole companion objects. But then c.inferImplicitVal should not lookup any implicits, for the same reasons..
  • based on previous clue - maybe compiler collect informations about implicits and scopes incrementally, during some typechecks or something like that? And static annotation expansions is done before phase in which compiler scan implicit vals within enclosure?

Edit:
AFAIK def macros are an (experimental) part of scala compiler, while annotation macros are still available via macroparadise plugin. Maybe this is the reason?

@xeno-by could you elaborate on this?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kciesielski The issue with c.typecheck (and c.inferImplicitValue that's essentially based on c.typecheck) in macro annotations is a yet unsolved problem in the design of macro annotations. That's indeed a bug.

@mkubala Your high-level hypothesis about the reason for this limitation is spot-on (if you're interested in details, scalamacros/paradise#14 provides a lot of information on the nature of the issue).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for explanation 👍

}

}
35 changes: 3 additions & 32 deletions macros/src/main/scala/com/softwaremill/macmemo/memoizeMacro.scala
Original file line number Diff line number Diff line change
Expand Up @@ -37,34 +37,6 @@ object memoizeMacro {

val enclosure = c.enclosingClass

def resolveMemoBuilder: Tree = {

def inferImplicitBuilderVal: Tree = {

def findImplicitBuilderVal(body: List[Tree]): Tree =
body.collect {
case v @ ValDef(m, name, _, rhs)
if (m.hasFlag(Flag.IMPLICIT) && c.typecheck(rhs).tpe <:< typeOf[MemoCacheBuilder]) =>
Ident(name)
}.lastOption.getOrElse(EmptyTree)

enclosure match {
case ClassDef(_, _, _, Template(_, _, body)) => findImplicitBuilderVal(body)
case ModuleDef(_, _, Template(_, _, body)) => findImplicitBuilderVal(body)
case oth => EmptyTree
}
}

def bringDefaultBuilder: Tree = {
c.info(c.enclosingPosition, s"Cannot find custom memo builder for method '${cachedMethodId.methodName}' - default builder will be used", false)
reify {
MemoCacheBuilder.guavaMemoCacheBuilder
}.tree
}

c.inferImplicitValue(typeOf[MemoCacheBuilder]) orElse inferImplicitBuilderVal orElse bringDefaultBuilder
}

def buildCacheBucketId: Tree = {
val enclosingClassSymbol = enclosure.symbol
val enclosureFullName = enclosingClassSymbol.fullName + (if (enclosingClassSymbol.isModule) "$." else ".")
Expand All @@ -79,10 +51,9 @@ object memoizeMacro {
q"""com.softwaremill.macmemo.MemoizeParams($maxSize, ${ttl.toMillis}, $concurrencyLevelOpt)"""
}

val t = appliedType(weakTypeOf[Cache[Any]].typeConstructor, typeOf[List[Any]] :: Nil)
ValDef(Modifiers(Flag.LAZY), cachedMethodId.generatedMemoValName, TypeTree(t),
Apply(Select(resolveMemoBuilder, TermName("build")), List(buildCacheBucketId, buildParams))
)
q"""lazy val ${cachedMethodId.generatedMemoValName}: com.softwaremill.macmemo.Cache[List[Any]] =
com.softwaremill.macmemo.BuilderResolver.resolve($buildCacheBucketId).build($buildCacheBucketId, $buildParams)"""

}

def injectCacheUsage(cachedMethodId: MemoIdentifier, function: DefDef) = {
Expand Down