如何在Scala3中编译和执行Scala代码?

5

我想在运行时使用Scala3编译和执行以字符串形式给出的Scala代码。就像在Scala 2中一样,我会使用Reflection。

import scala.reflect.runtime.universe as ru
import scala.tools.reflect.ToolBox
val scalaCode = q"""println("Hello world!")"""
val evalMirror = ru.runtimeMirror(this.getClass.getClassLoader)
val toolBox = evalMirror.mkToolBox()
toolBox.eval(scalaCode) //Hello world!

如果我尝试在Scala3中运行此代码,我会得到以下错误:

Scala 2 macro cannot be used in Dotty. See https://dotty.epfl.ch/docs/reference/dropped-features/macros.html
To turn this error into a warning, pass -Xignore-scala2-macros to the compiler

我如何将这段代码翻译成 Scala3?


1
你读过Scala 3元编程文档吗?尝试了什么吗? - Gaël J
1
@GaëlJ 是的,我看过了,但它大部分都是不完整的,而且很多链接都已经失效了。我理解得很少,所以我正在寻求帮助。你知道怎么做吗? - user3476509
3
为什么你要这样做?运行时反射应该只在没有其他解决方案的情况下使用,这种情况非常非常罕见。 - cchantep
2
请将损坏的链接报告给 https://github.com/scala/docs.scala-lang/issues - Seth Tisue
我们想说的是,您应该在为什么想要这样做方面给我们提供更多背景信息。根据原因,我们将能够为您提供有关如何使用Scala 3实现目标的建议。 - Gaël J
@user3476509 实际上,你的代码已经非常接近工作了。只需要将 q"..." (Scala 2 宏)替换为 toolbox.parse("...") 即可。请参见更新。 - Dmytro Mitin
1个回答

11

这篇回答的Scala 2版本在这里:如何在脚本运行时运行生成的代码?

在Scala 3中:

ammonite.Main(verboseOutput = false).runCode("""println("Hello, World!")""")
// Hello, World!

build.sbt

scalaVersion := "3.1.3"
libraryDependencies += "com.lihaoyi" % "ammonite" % "2.5.4-22-4a9e6989" cross CrossVersion.full
excludeDependencies ++= Seq(
  ExclusionRule("com.lihaoyi", "sourcecode_2.13"),
  ExclusionRule("com.lihaoyi", "fansi_2.13"),
)
com.eed3si9n.eval.Eval()
  .evalInfer("""println("Hello, World!")""")
  .getValue(this.getClass.getClassLoader)
// Hello, World!

build.sbt

scalaVersion := "3.2.0"
libraryDependencies += "com.eed3si9n.eval" % "eval" % "0.1.0" cross CrossVersion.full
  • 或者您可以尝试我的实现
com.github.dmytromitin.eval.Eval[Unit]("""println("Hello, World!")""")
// Hello, World!

scalaVersion := "3.2.1"
libraryDependencies += "com.github.dmytromitin" %% "eval" % "0.1"
  • 你也可以使用标准的Scala 3 REPL解释器
dotty.tools.repl.ScriptEngine().eval("""println("Hello, World!")""")
// Hello, World!

build.sbt

scalaVersion := "3.1.3"
libraryDependencies += scalaOrganization.value %% "scala3-compiler" % scalaVersion.value
  • 如果您有一个scala.quoted.Expr '{...} (一个静态类型的包装器,覆盖了一个抽象语法树scala.quoted.Quotes#Tree),而不是普通的字符串,那么您可以使用运行时多层次
import scala.quoted.*
given staging.Compiler = staging.Compiler.make(getClass.getClassLoader)
staging.run('{ println("Hello, World!") })
// Hello, World!

build.sbt

scalaVersion := "3.1.3"
libraryDependencies += scalaOrganization.value %% "scala3-staging" % scalaVersion.value
  • 以上所有内容都是为了在Scala 3中运行Scala 3代码。如果我们想在Scala 3中运行Scala 2代码,那么我们仍然可以使用Scala 2反射工具箱。Scala 2宏不起作用,因此我们不能使用runtime.currentMirrorq"...",但可以使用universe.runtimeMirrortb.parse
import scala.tools.reflect.ToolBox // implicit 

val tb = scala.reflect.runtime.universe
  .runtimeMirror(getClass.getClassLoader)
  .mkToolBox()
tb.eval(tb.parse("""println("Hello, World!")"""))
// Hello, World!

build.sbt

scalaVersion := "3.1.3"
libraryDependencies ++= scalaOrganization.value % "scala-compiler" % "2.13.8"
  • 除此之外,您还可以使用标准的Scala 2 REPL解释器在Scala 3中运行Scala 2代码。
scala.tools.nsc.interpreter.shell.Scripted()
  .eval("""System.out.println("Hello, World!")""")
// Hello, World!

build.sbt

scalaVersion := "3.1.3"
libraryDependencies ++= scalaOrganization.value % "scala-compiler" % "2.13.8"
  • 同时,您还可以使用JSR223脚本。根据您的类路径中是否有scala3-compilerscala-compiler,您将运行Scala 3或Scala 2(其中一个脚本引擎:Scala 3 dotty.tools.repl.ScriptEngine或Scala 2 scala.tools.nsc.interpreter.shell.Scripted)。如果您添加了两个依赖项,则先添加的依赖项将生效。
new javax.script.ScriptEngineManager(getClass.getClassLoader)
  .getEngineByName("scala")
  .eval("""println("Hello, World!")""")
// Hello, World!

如果您想更好地控制使用哪个依赖项(而无需重新导入项目),您可以使用Coursier并指定类加载器。

import coursier.* // libraryDependencies += "io.get-coursier" %% "coursier" % "2.1.0-M6-53-gb4f448130" cross CrossVersion.for3Use2_13
val files = Fetch()
  .addDependencies(
    Dependency(Module(Organization("org.scala-lang"), ModuleName("scala3-compiler_3")), "3.2.0"),
    // Dependency(Module(Organization("org.scala-lang"), ModuleName("scala-compiler")), "2.13.9")
  )
  .run()

val classLoader = new java.net.URLClassLoader(
  files.map(_.toURI.toURL).toArray,
  /*getClass.getClassLoader*/null // ignoring current classpath
)
new javax.script.ScriptEngineManager(classLoader)
  .getEngineByName("scala")
  .eval("""
    type T = [A] =>> [B] =>> (A, B) // Scala 3
    //type T = List[Option[A]] forSome {type A} // Scala 2
    System.out.println("Hello, World!")
  """)
// Hello, World!
  • 你可以使用实际编译器在Scala 3中自己实现Eval
import dotty.tools.io.AbstractFile
import dotty.tools.dotc.core.Contexts.Context
import dotty.tools.dotc.Driver
import dotty.tools.dotc.util.SourceFile
import dotty.tools.io.{VirtualDirectory, VirtualFile}
import java.net.URLClassLoader
import java.nio.charset.StandardCharsets
import dotty.tools.repl.AbstractFileClassLoader
import scala.io.Codec
import coursier.{Dependency, Module, Organization, ModuleName, Fetch}

  // we apply usejavacp=true instead
//  val files = Fetch()
//    .addDependencies(
//       Dependency(Module(Organization("org.scala-lang"), ModuleName("scala3-compiler_3")), "3.1.3"),
//    )
//    .run()
//
//  val depClassLoader = new URLClassLoader(
//    files.map(_.toURI.toURL).toArray,
//    /*getClass.getClassLoader*/ null // ignoring current classpath
//  )

val code =
  s"""
     |package mypackage
     |
     |object Main {
     |  def main(args: Array[String]): Unit = {
     |    println("Hello, World!")
     |  }
     |}""".stripMargin

val outputDirectory = VirtualDirectory("(memory)")
compileCode(code, List()/*files.map(f => AbstractFile.getFile(f.toURI.toURL.getPath)).toList*/, outputDirectory)
val classLoader = AbstractFileClassLoader(outputDirectory, this.getClass.getClassLoader/*depClassLoader*/)
runObjectMethod("mypackage.Main", classLoader, "main", Seq(classOf[Array[String]]), Array.empty[String])
// Hello, World!

def compileCode(
                 code: String,
                 classpathDirectories: List[AbstractFile],
                 outputDirectory: AbstractFile
               ): Unit = {
  class DriverImpl extends Driver {
    private val compileCtx0 = initCtx.fresh
    given Context = compileCtx0.fresh
      .setSetting(
        compileCtx0.settings.classpath,
        classpathDirectories.map(_.path).mkString(":")
      ).setSetting(
        compileCtx0.settings.usejavacp,
        true
      ).setSetting(
        compileCtx0.settings.outputDir,
        outputDirectory
      )
    val compiler = newCompiler
  }

  val driver = new DriverImpl
  import driver.given Context

  val sourceFile = SourceFile(VirtualFile("(inline)", code.getBytes(StandardCharsets.UTF_8)), Codec.UTF8)
  val run = driver.compiler.newRun
  run.compileSources(List(sourceFile))
  // val unit = run.units.head
  // println("untyped tree=" + unit.untpdTree)
  // println("typed tree=" + unit.tpdTree)
}

def runObjectMethod(
                     objectName: String,
                     classLoader: ClassLoader,
                     methodName: String,
                     paramClasses: Seq[Class[?]],
                     arguments: Any*
                   ): Any = {
  val clazz = Class.forName(s"$objectName$$", true, classLoader)
  val module = clazz.getField("MODULE$").get(null)
  val method = module.getClass.getMethod(methodName, paramClasses*)
  method.invoke(module, arguments*)
}

(previous version)

build.sbt

scalaVersion := "3.1.3"
libraryDependencies += scalaOrganization.value %% "scala3-compiler" % scalaVersion.value

另请参见:从Scala 3宏中获取类的注释(在Scala 3中进行多级编程的黑客和实现我们自己的eval代替Scala 2中禁止在Scala 3宏中使用的context.evalstaging.run)。

  • 另请参见

Scala Presentation Compiler介绍

将字符串中的Scala 3代码解析为Scala 3 AST并在运行时执行

Scala 3反射

帮助使用dotty编译器和运行时类加载


https://github.com/propensive/fury/blob/main/src/core/compilation.scala - Dmytro Mitin
1
非常好,谢谢。但是如果我们想要从一个字符串创建一个 Expr[T] 而不仅仅是运行它并接收其计算值呢?是否可以将字符串解析为类型化表达式,例如将某些文件内容作为字符串加载,然后在运行之前对其进行解析和分析? - shvahabi
这个!谢谢 @DmytroMitin - Joan
你找到方法了吗,@shvahabi? - Joan
亲爱的@Joan,根据我的研究,似乎Scala3的设计者有意从元编程设计议程中删除了这个功能(例如,请参见https://contributors.scala-lang.org/t/compatibility-required-for-migration-from-scala2-macro/5100/2,来自Odersky教授)。这样的设计理念是合理的,因为创建只接受特殊Scala3代码片段字符串并忽略其他有效片段的解释器应该根据应用程序需求进行,并且是开发人员的任务,因为它涉及许多应用程序设计决策。 - shvahabi
另一方面,能够从任意有效的Scala3代码片段字符串中创建表达式,会降低可读性并降低可扩展性。不过,如果您的应用程序要求如此,您可以创建一个足够大的解释器来包含整个Scala3语言结构。Scala3设计者仅提供了基础设施,以系统地嵌入预期子集的Scala3语言结构的字符串解释器,因为实际上没有真实世界的应用程序需要嵌入整个Scala3语言解释器。 - shvahabi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接