Jelly Terra
Jelly Terra

Reputation: 11

Passing string as the parameter to ValDef left-hand-side's method invoke inside quote block in Scala 3 macro annotation

I am trying implementing a macro annotation with such effect:

@MarkNode
val dataAbc = new Data()

which generates

val dataAbc = new Data()
dataAbc.setCodegenSuffix("dataAbc")

And, some of examples used the form like below

@experimental
class MarkNode extends MacroAnnotation:
  override def transform(using quotes: Quotes)(tree: quotes.reflect.Definition): List[quotes.reflect.Definition] =
    import quotes.reflect.*

What should the type be when I want the macro generates Expr[Something] instead of Definitions? With Scala version 3.3.4 So that package in use is org.scala-lang.scala3-compiler-3.3.4-provided

tree match {
  case ValDef(lhs, tpt, rhs) =>
    val term = Ident.copy(tree)(lhs).asExprOf[Data]
    val invoke = '{ $term.setCodegenSuffix('lhs) }
symbol literal 'lhs is no longer supported,
use a string literal "lhs" or an application Symbol("lhs") instead,
or enclose in braces '{lhs} if you want a quoted expression.
For now, you can also `import language. deprecated.
symbolLiterals` to accept the idiom,
but this possibility might no longer be available in the future.

But Symbol did not have an apply function.

And the LSP reported error that the usage of 'lhs is no longer supported. I don't want deprecated features. So what should I do to pass the lhs which is typed in String to the method call in quoted block?

Full snippet:

import scala.annotation.{MacroAnnotation, experimental}
import scala.collection.mutable.ListBuffer
import scala.quoted.*

@experimental
class MarkNode extends MacroAnnotation:
  override def transform(using quotes: Quotes)(tree: quotes.reflect.Definition): List[quotes.reflect.Definition] =
    import quotes.reflect.*

    val buffer = new ListBuffer[Definition]()

    tree match {
      case ValDef(lhs, tpt, rhs) =>
        val data = Ident.copy(tree)(lhs).asExprOf[Data]
        val invoke = '{ $data.setCodegenSuffix('lhs) }
    }
    
    buffer.result() // Well I think it's wrong. Because `invoke` is obviously an Expr[Unit]

Scala's way of meta programming seems to be more flex, but it's really not friendly to newbies. I've referenced many examples and one of is via asTerm but it seems that Scala 3 versions are varying so that some of the types those examples used cannot be found. And documentation Scala 3 now has seems did not cover such usage. So sad I really have no other way? :(

Upvotes: 1

Views: 55

Answers (1)

Mateusz Kubuszok
Mateusz Kubuszok

Reputation: 27595

Ok, there are several things to unpack.

First, Symbols. Older versions of Scala defined Symbol as a way of having Strings but unique, with O(1) comparison etc. AFAIK it was idea borrowed from Closure/Ruby which didn't catch on Scala. There was also a symbol literal - any identifier with ' became symbol - e.g. 'name.

It became problematic with Scala 3 quotes because they are being created the same way, with ', which is why you have the error

symbol literal 'lhs is no longer supported,
use a string literal "lhs" or an application Symbol("lhs") instead,
or enclose in braces '{lhs} if you want a quoted expression.
For now, you can also `import language. deprecated.
symbolLiterals` to accept the idiom,
but this possibility might no longer be available in the future.

But, as the error message says, you can avoid the problem by using '{ sth } instead of 'sth.

The other issue is that '{ $data.setCodegenSuffix('lhs) }, even when rewritten to '{ ${ data }.setCodegenSuffix('{ lhs }) }, is still wrong - you would like to unquote (${}) lhs, not quote it ('{}) like you did.

So it should be '{ ${ data }.setCodegenSuffix(${ lhs }) }.

But lhs would have to be Expr[String], not String, there is a way of turning String into Expr, but you have to call it explicitly: Expr(lhs).

Finally, macro annotations take Definitions and return Definitions - you cannot add expressions/statements in arbitrary places. But you can achieve quote a lot but changing how definitions are being defined.

You cannot turn

@MarkNode
val dataAbc = new Data()

into

val dataAbc = new Data()
dataAbc.setCodegenSuffix("dataAbc") // outside Definition

but you can turn it into

val dataAbc = {
  val tmp = new Data()
  tmp.setCodegenSuffix("dataAbc") // inside Definition
  tmp
}

Now, all that we need is to put all of that together (and handle that unmatched cases:

import scala.annotation.{MacroAnnotation, experimental}
import scala.quoted.*

// to make my example compile and type check
class Data {

  def setCodegenSuffix(name: String): Unit = ()
}

@experimental
class MarkNode extends MacroAnnotation {

  override def transform(using quotes: Quotes)(
      tree: quotes.reflect.Definition
  ): List[quotes.reflect.Definition] = {
    import quotes.*, quotes.reflect.*

    tree match {
      // We need to ensure: an initial value is there, the annotation is on a value of Data
      case ValDef(name, tpt, Some(initialValue)) if tpt.tpe =:= TypeRepr.of[Data] => 
        val newInitialValue = '{
          val tmp: Data = ${ initialValue.asExprOf[Data] }
          tmp.setCodegenSuffix(${ Expr(name) })
          tmp
        }

        List(ValDef(tree.symbol, Some(newInitialValue.asTerm)))
      case _ =>
        report.errorAndAbort("Macro only supported on vals of Data")
    }
  }
}

I haven't checked if this works, but that the closest I could do to match what you wanted, you can adjust it further for your needs.

Upvotes: 1

Related Questions