[FIXED] Können Sie dsinfo in Scala 3 implementieren? (Können Scala 3-Makros Informationen über ihren Kontext erhalten?)

Ausgabe

Mit der dsinfoBibliothek können Sie mithilfe von Scala 2-Makros auf die Namen von Werten aus dem Kontext zugreifen, in dem eine Funktion geschrieben wurde. Das Beispiel, das sie geben, ist, dass, wenn Sie so etwas haben

val name = myFunction(x, y)

myFunctiontatsächlich wird der Name von its valzusätzlich zu den anderen Argumenten übergeben, dh myFunction("name", x, y).

Dies ist sehr nützlich für DSLs, bei denen Sie benannte Werte für die Fehlerberichterstattung oder andere Arten der Codierung möchten. Die einzige andere Option scheint den Namen explizit als zu übergeben String, was zu unbeabsichtigten Diskrepanzen führen kann.

Ist dies mit Scala 3-Makros möglich, und wenn ja, wie “klettern” Sie in den Baum am Verwendungsort des Makros, um seine ID zu finden?

Lösung

In Scala 3 gibt es keine c.macroApplication. Nur Position.ofMacroExpansionanstelle eines Baumes. Aber wir können analysieren Symbol.spliceOwner.maybeOwner. Ich vermute das scalacOptions += "-Yretain-trees"ist eingeschaltet.

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]
  }
}

Verwendungszweck:

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"))
}

dsinfobehandelt auch den Namen twoargsbei der Aufrufseite (als Vorlage $macro), aber ich habe dies nicht implementiert. Ich denke, der Name (falls erforderlich) kann von abgerufen werden Position.ofMacroExpansion.sourceCode.


Aktualisieren. Hier ist der Implementierungshandhabungsname der Inline-Methode (z. B. twoargs) unter Verwendung von Scalameta + Semanticdb neben Scala 3-Makros.

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 [email protected](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,
  )

By the way, Semantidb can’t be replaced by Tasty here because when a macro in App is being expanded, the file App.scala.semantidb already exists (it’s generated early, at frontend phase of compilation) but App.tasty hasn’t yet (it appears when App has been compiled i.e. after expansion of the macro, at pickler phase).

.scala.semanticdb file will appear even if .scala file doesn’t compile (e.g. if there is an error in macro expansion) but .tasty file won’t.

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?

Wie erhalte ich den Typ einer Variablen mit Scalameta, wenn decltpe leer ist?


Siehe auch https://github.com/lampepfl/dotty-macro-examples/tree/main/accessEnclosingParameters


Beantwortet von –
Dmytro Mitin


Antwort geprüft von –
Senaida (FixError Volunteer)

0 Shares:
Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like