Search code examples
scalascala-macros

macro implementation not found (scala 2.13.3)


I'm using the RunningStrategies class in my Scala code to define a set of running strategies. To create an instance of RunningStrategies, I'm using the apply method along with a variable number of arguments representing the different strategies. To achieve this, I have also defined a macro called RunningStrategiesMacros.

However, when I try to use RunningStrategies with some strategies, I get the following error message: "macro implementation not found: apply". The error seems to be related to the fact that I'm trying to use the macro implementation in the same compilation run that defines it.

RunningStrategy:

package com.dv.phoenix.brandsafety.models

object RunningStrategy extends Enumeration {
  type RunningStrategy = Value

  val
  MonitoringOnly,
  BlockingOnly,
  MonitoringAndBlockingInherent,
  MonitoringAndBlockingRecalculate
  = Value
}

RunningStrategies:

package com.dv.phoenix.brandsafety.models

import com.dv.phoenix.brandsafety.models.RunningStrategy.RunningStrategy
import com.dv.phoenix.brandsafety.utils.RunningStrategiesMacros

import scala.language.experimental.macros
case class RunningStrategies private (runningStrategies: Set[RunningStrategy])
object RunningStrategies {
  def apply(strategies: RunningStrategy*): RunningStrategies = macro RunningStrategiesMacros.applyImp
}

RunningStrategiesMacro:

package com.dv.phoenix.brandsafety.utils

import com.dv.phoenix.brandsafety.models.RunningStrategies
import com.dv.phoenix.brandsafety.models.RunningStrategy.RunningStrategy

import scala.reflect.macros.blackbox

object RunningStrategiesMacros {
   def applyImp(c: blackbox.Context)(runningStrategies: c.Expr[RunningStrategy]*): c.Expr[RunningStrategies] = {
    import c.universe._

    val hasMonitoringAndBlockingInherent = runningStrategies.map(_.tree.toString).contains(s"${RunningStrategy.getClass.getName.stripSuffix("$")}.${RunningStrategy.MonitoringAndBlockingInherent}")
    val hasMonitoringAndBlockingRecalculate = runningStrategies.map(_.tree.toString).contains(s"${RunningStrategy.getClass.getName.stripSuffix("$")}.${RunningStrategy.MonitoringAndBlockingRecalculate}")

    if (hasMonitoringAndBlockingInherent && hasMonitoringAndBlockingRecalculate) {
      c.abort(c.enclosingPosition, "runningStrategies cannot include both RunningStrategy.MonitoringAndBlockingInherent and RunningStrategy.MonitoringAndBlockingRecalculate")
    } else {
      c.Expr(q"RunningStrategies(Set(..$runningStrategies))")
    }
  }

}

Usuage:

override protected val runningStrategies: RunningStrategies = RunningStrategies(MonitoringOnly, MonitoringAndBlockingInherent, MonitoringAndBlockingRecalculate)

I got the following error:

macro implementation not found: apply
(the most common reason for that is that you cannot use macro implementations in the same compilation run that defines them)
  override protected val runningStrategies: RunningStrategies = RunningStrategies(MonitoringOnly, MonitoringAndBlockingInherent, MonitoringAndBlockingRecalculate)

I have noticed that the scala-logging library has managed to solve this problem with their LoggerImpl and LoggerMacro classes. Can someone explain how they were able to do this and how I can apply the same technique to my RunningStrategiesMacro?


Solution

  • In your question you described your package structure but not your project structure. Packages and subprojects are different concepts

    Why can't sbt/assembly find the main classes in a project with multiple subprojects?

    You should have different subprojects if you want to use macros in Scala 2

    https://www.scala-sbt.org/1.x/docs/Macro-Projects.html

    Firstly, let's refresh the terminology. This is a macro definition

    import scala.language.experimental.macros
    
    def foo(): Unit = macro fooImpl
    

    This is a macro implementation

    import scala.reflect.macros.blackbox
    
    def fooImpl(c: blackbox.Context)(): c.Tree = {
      import c.universe._
      println("test")
      q"""_root_.scala.Predef.println("foo")"""
    }
    

    This is a macro application (call site, macro expansion)

    foo()
    // at compile time: scalac: test
    // at runtime: foo
    

    In Scala 2 macro implementations must be compiled before macro applications (the compile time of macro applications i.e. macro expansion is the runtime of macros). So it's macro implementations and macro applications that must be in different compile units. For example in different subprojects of your project. Or in src/main/scala and src/test/scala of the same project (main code is compiled before tests). Where macro definitions are is not so important. They can be in the same compilation unit/subproject as macro implementations (maybe in the same file/class) or in the same as macro applications or in their own.

    In scala-logging, macro definitions

    def error(message: String): Unit = macro LoggerMacro.errorMessage
    

    and macro implementations

    def errorMessage(c: LoggerContext)(message: c.Expr[String]): c.universe.Tree = ...
    

    are in the same project (in different files but this is not necessary, they can be in the same file) but macro applications

    logger.error(msg)
    

    are in src/test/scala https://github.com/lightbend-labs/scala-logging/blob/v3.9.5/src/test/scala/com/typesafe/scalalogging/LoggerSpec.scala . And other macro applications will be also in different compilation units i.e. client projects using scala-logging as a dependency.

    In your project

    def apply(strategies: RunningStrategy*): RunningStrategies = macro RunningStrategiesMacros.applyImp
    

    is a macro definition and

    def applyImp(c: blackbox.Context)(strategies: c.Expr[RunningStrategy]*): c.Expr[RunningStrategies] = ...
    

    is a macro implementation. But you should re-organize your project having at least two subprojects and place macro applications

    override protected val runningStrategies: RunningStrategies = RunningStrategies.apply(MonitoringOnly, MonitoringAndBlockingInherent, MonitoringAndBlockingRecalculate)
    

    to a different subproject

    lazy val commonSettings = Seq(
      scalaVersion := "2.13.10"
    )
    
    // where you override runningStrategies goes here
    lazy val core = project
      .dependsOn(macros)
      .settings(
        commonSettings,
        scalacOptions += "-Ymacro-debug-lite" // convenient to debug macro expansions
      )
    
    // utils/RunningStrategiesMacros go here
    lazy val macros = project
      .settings(
        commonSettings,
        // necessary for macro implementations
        libraryDependencies += scalaOrganization.value % "scala-reflect" % scalaVersion.value
      )
    

    Is there any trick to use macros in the same file they are defined?

    Possible to identify/use Scala macros using reflection or similar?

    Using scala macro from java

    Implicit materialization in the same module

    Def macro - scala 2.13 - not found: value cond

    Scala macros and separate compilation units

    Auto-Generate Companion Object for Case Class in Scala