你能在Scala3中实现dsinfo吗?(Scala 3宏可以获取有关其上下文的信息吗?)

qij5mzcb  于 2022-11-09  发布在  Scala
关注(0)|答案(1)|浏览(174)

dsinfo library允许您从使用Scala2宏编写函数的上下文中访问值的名称。他们给出的例子是,如果你有类似的东西

val name = myFunction(x, y)

除了其他参数外,myFunction实际上还将被传递其val的名称,即myFunction("name", x, y)
这对于DSL非常有用,因为您希望将命名值用于错误报告或其他类型的编码。唯一的另一种选择似乎是显式地将名称作为String传递,这可能会导致无意中的不匹配。
对于Scala 3宏,这是可能的吗?如果可能,如何在宏的使用位置“爬上”树来找到它的id?

vd8tlhqk

vd8tlhqk1#

在Scala3中没有c.macroApplication。只有Position.ofMacroExpansion而不是树。但我们可以分析Symbol.spliceOwner.maybeOwner。我假设scalacOptions += "-Yretain-trees"已打开。

import scala.annotation.experimental
import scala.quoted.*

object Macro {
  inline def makeCallWithName[T](inline methodName: String): T = 
    ${makeCallWithNameImpl[T]('methodName)}

  @experimental
  def makeCallWithNameImpl[T](methodName: Expr[String])(using Quotes, Type[T]): Expr[T] = {
    import quotes.reflect.*
    println(Position.ofMacroExpansion.sourceCode)//Some(twoargs(1, "one"))

    val methodNameStr = methodName.valueOrAbort
    val strs = methodNameStr.split('.')
    val moduleName = strs.init.mkString(".")
    val moduleSymbol = Symbol.requiredModule(moduleName)

    val shortMethodName = strs.last
    val ident = Ident(TermRef(moduleSymbol.termRef, shortMethodName))

    val (ownerName, ownerRhs) = Symbol.spliceOwner.maybeOwner.tree match {
      case ValDef(name, tpt, Some(rhs)) => (name, rhs)
      case DefDef(name, paramss, tpt, Some(rhs)) => (name, rhs)
      case t => report.errorAndAbort(s"can't find RHS of ${t.show}")
    }

    val treeAccumulator = new TreeAccumulator[Option[Tree]] {
      override def foldTree(acc: Option[Tree], tree: Tree)(owner: Symbol): Option[Tree] = tree match {
        case Apply(fun, args) if fun.symbol.fullName == "App$.twoargs" =>
          Some(Apply(ident, Literal(StringConstant(ownerName)) :: args))
        case _ => foldOverTree(acc, tree)(owner)
      }
    }
    treeAccumulator.foldTree(None, ownerRhs)(ownerRhs.symbol)
      .getOrElse(report.errorAndAbort(s"can't find twoargs in RHS: ${ownerRhs.show}"))
      .asExprOf[T]
  }
}

用途:

package mypackage
case class TwoArgs(name : String, i : Int, s : String)
import mypackage.TwoArgs

object App {
  inline def twoargs(i: Int, s: String) = 
    Macro.makeCallWithName[TwoArgs]("mypackage.TwoArgs.apply")

  def x() = twoargs(1, "one") // TwoArgs("x", 1, "one")

  def aMethod() = {
    val y = twoargs(2, "two") // TwoArgs("y", 2, "two")
  }

  val z = Some(twoargs(3, "three")) // Some(TwoArgs("z", 3, "three"))
}

dsinfo还在调用点处理名称twoargs(作为模板$macro),但我没有实现这一点。我猜这个名字(如果有必要的话)可以从Position.ofMacroExpansion.sourceCode获得。

**更新。**这里是内联方法(例如twoargs)的实现处理名称,除了Scala 3宏之外,还使用了Scalameta+Semancdb。

import mypackage.TwoArgs

object App {
  inline def twoargs(i: Int, s: String) =
    Macro.makeCallWithName[TwoArgs]("mypackage.TwoArgs.apply")

  inline def twoargs1(i: Int, s: String) =
    Macro.makeCallWithName[TwoArgs]("mypackage.TwoArgs.apply")

  def x() = twoargs(1, "one") // TwoArgs("x", 1, "one")

  def aMethod() = {
    val y = twoargs(2, "two") // TwoArgs("y", 2, "two")
  }

  val z = Some(twoargs1(3, "three")) // Some(TwoArgs("z", 3, "three"))
}
package mypackage

case class TwoArgs(name : String, i : Int, s : String)
import scala.annotation.experimental
import scala.quoted.*

object Macro {
  inline def makeCallWithName[T](inline methodName: String): T =
    ${makeCallWithNameImpl[T]('methodName)}

  @experimental
  def makeCallWithNameImpl[T](methodName: Expr[String])(using Quotes, Type[T]): Expr[T] = {
    import quotes.reflect.*

    val position = Position.ofMacroExpansion
    val scalaFile = position.sourceFile.getJPath.getOrElse(
      report.errorAndAbort(s"maybe virtual file, can't find path to position $position")
    )
    val inlineMethodSymbol =
      new SemanticdbInspector(scalaFile)
        .getInlineMethodSymbol(position.start, position.end)
        .getOrElse(report.errorAndAbort(s"can't find Scalameta symbol at position (${position.startLine},${position.startColumn})..(${position.endLine},${position.endColumn})=$position"))

    val methodNameStr = methodName.valueOrAbort
    val strs = methodNameStr.split('.')
    val moduleName = strs.init.mkString(".")
    val moduleSymbol = Symbol.requiredModule(moduleName)

    val shortMethodName = strs.last
    val ident = Ident(TermRef(moduleSymbol.termRef, shortMethodName))

    val owner = Symbol.spliceOwner.maybeOwner

    val macroApplication: Option[Tree] = {
      val (ownerName, ownerRhs) = owner.tree match {
        case ValDef(name, tpt, Some(rhs)) => (name, rhs)
        case DefDef(name, paramss, tpt, Some(rhs)) => (name, rhs)
        case t => report.errorAndAbort(s"can't find RHS of ${t.show}")
      }

      val treeAccumulator = new TreeAccumulator[Option[Tree]] {
        override def foldTree(acc: Option[Tree], tree: Tree)(owner: Symbol): Option[Tree] = tree match {
          case Apply(fun, args) if tree.pos == position /* fun.symbol.fullName == inlineMethodSymbol */ =>
            Some(Apply(ident, Literal(StringConstant(ownerName)) :: args))
          case _ => foldOverTree(acc, tree)(owner)
        }
      }
      treeAccumulator.foldTree(None, ownerRhs)(ownerRhs.symbol)
    }

    val res = macroApplication
      .getOrElse(report.errorAndAbort(s"can't find application of $inlineMethodSymbol in RHS of $owner"))
    report.info(res.show)
    res.asExprOf[T]
  }
}
import java.nio.file.{Path, Paths}
import scala.io
import scala.io.BufferedSource
import scala.meta.*
import scala.meta.interactive.InteractiveSemanticdb
import scala.meta.internal.semanticdb.{ClassSignature, Locator, Range, SymbolInformation, SymbolOccurrence, TextDocument, TypeRef}

class SemanticdbInspector(val scalaFile: Path) {
  val scalaFileStr = scalaFile.toString

  var textDocuments: Seq[TextDocument] = Seq()
  Locator(
    Paths.get(scalaFileStr + ".semanticdb")
  )((path, textDocs) => {
    textDocuments ++= textDocs.documents
  })

  val bufferedSource: BufferedSource = io.Source.fromFile(scalaFileStr)
  val source = try bufferedSource.mkString finally bufferedSource.close()

  extension (tree: Tree) {
    def occurence: Option[SymbolOccurrence] = {
      val treeRange = Range(tree.pos.startLine, tree.pos.startColumn, tree.pos.endLine, tree.pos.endColumn)
      textDocuments.flatMap(_.occurrences)
        .find(_.range.exists(occurrenceRange => treeRange == occurrenceRange))
    }

    def info: Option[SymbolInformation] = occurence.flatMap(_.symbol.info)
  }

  extension (symbol: String) {
    def info: Option[SymbolInformation] = textDocuments.flatMap(_.symbols).find(_.symbol == symbol)
  }

  def getInlineMethodSymbol(startOffset: Int, endOffset: Int): Option[String] = {
    def translateScalametaToMacro3(symbol: String): String =
      symbol
        .stripPrefix("_empty_/")
        .stripSuffix("().")
        .replace(".", "$.")
        .replace("/", ".")

    dialects.Scala3(source).parse[Source].get.collect {
      case t@Term.Apply(fun, args) if t.pos.start == startOffset && t.pos.end == endOffset =>
        fun.info.map(_.symbol)
    }.headOption.flatten.map(translateScalametaToMacro3)
  }
}
lazy val scala3V = "3.1.3"
lazy val scala2V = "2.13.8"
lazy val scalametaV = "4.5.13"

lazy val root = project
  .in(file("."))
  .settings(
    name := "scala3demo",
    version := "0.1.0-SNAPSHOT",
    scalaVersion := scala3V,
    libraryDependencies ++= Seq(
      "org.scalameta" %% "scalameta" % scalametaV cross CrossVersion.for3Use2_13,
      "org.scalameta" % s"semanticdb-scalac_$scala2V" % scalametaV,
    ),
    scalacOptions ++= Seq(
      "-Yretain-trees",
    ),
    semanticdbEnabled := true,
  )

顺便说一句,Semantidb在这里不能用Tasty代替,因为当App中的一个宏被展开时,文件App.scala.semantidb已经存在(它是在编译的前端阶段生成的early),但App.tasty还没有(它出现在App已经编译时,即在宏展开之后,在Pickler阶段)。
即使.scala文件没有编译(例如,如果宏展开有错误),.scala.semanticdb文件也会出现,但.tasty文件不会。
scala.meta parent of parent of Defn.Object
Is it possible to using macro to modify the generated code of structural-typing instance invocation?
Scala conditional compilation
Macro annotation to override toString of Scala function
How to merge multiple imports in scala?
How to get the type of a variable with scalameta if the decltpe is empty?
另请参阅https://github.com/lampepfl/dotty-macro-examples/tree/main/accessEnclosingParameters
简化版本:

import scala.quoted.*

inline def makeCallWithName[T](inline methodName: String): T =
  ${makeCallWithNameImpl[T]('methodName)}

def makeCallWithNameImpl[T](methodName: Expr[String])(using Quotes, Type[T]): Expr[T] = {
  import quotes.reflect.*

  val position = Position.ofMacroExpansion

  val methodNameStr = methodName.valueOrAbort
  val strs = methodNameStr.split('.')
  val moduleName = strs.init.mkString(".")
  val moduleSymbol = Symbol.requiredModule(moduleName)
  val shortMethodName = strs.last
  val ident = Ident(TermRef(moduleSymbol.termRef, shortMethodName))

  val owner0 = Symbol.spliceOwner.maybeOwner

  val ownerName = owner0.tree match {
    case ValDef(name, _, _) => name
    case DefDef(name, _, _, _) => name
    case t => report.errorAndAbort(s"unexpected tree shape: ${t.show}")
  }

  val owner = if owner0.isLocalDummy then owner0.maybeOwner else owner0

  val macroApplication: Option[Tree] = {
    val treeAccumulator = new TreeAccumulator[Option[Tree]] {
      override def foldTree(acc: Option[Tree], tree: Tree)(owner: Symbol): Option[Tree] = tree match {
        case _ if tree.pos == position => Some(tree)
        case _ => foldOverTree(acc, tree)(owner)
      }
    }
    treeAccumulator.foldTree(None, owner.tree)(owner)
  }

  val res = macroApplication.getOrElse(
    report.errorAndAbort("can't find macro application")
  ) match {
    case Apply(_, args) => Apply(ident, Literal(StringConstant(ownerName)) :: args)
    case t => report.errorAndAbort(s"unexpected shape of macro application: ${t.show}")
  }
  report.info(res.show)
  res.asExprOf[T]
}

相关问题