Ausgabe
Mit der dsinfo
Bibliothek 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)
myFunction
tatsächlich wird der Name von its val
zusä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.ofMacroExpansion
anstelle 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"))
}
dsinfo
behandelt auch den Namen twoargs
bei 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?
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)