在 GitHub 上编辑此页面

运行时多阶段编程

该框架同时表达了编译时元编程和多阶段编程。我们可以将编译时元编程视为一个两阶段编译过程:一个我们用顶层拼接编写代码的过程,该过程将用于代码生成(宏),另一个将在编译时执行所有必要的评估,以及一个我们通常会运行的对象程序。如果我们可以在运行时合成代码并为程序员提供一个额外的阶段,会怎样?然后,我们可以在运行时获得一个类型为Expr[T]的值,我们基本上可以将其视为一个类型化语法树,我们可以将其显示为字符串(漂亮打印)或编译并运行。如果引号数超过拼接数超过一个(实际上处理运行时类型为Expr[Expr[T]]Expr[Expr[Expr[T]]]、... 的值),那么我们讨论的是多阶段编程。

这种范例背后的动机是让运行时信息影响或指导代码生成。

直觉:代码运行的阶段由其嵌入的拼接范围和引号范围之间的差异决定。

  • 如果拼接多于引号,则代码在编译时运行,即作为宏。在一般情况下,这意味着运行一个解释器来评估代码,该代码表示为类型化抽象语法树。解释器在评估先前编译的方法的应用程序时可以回退到反射调用。如果拼接过剩超过一个,则意味着宏的实现代码(与它扩展到的代码相反)调用其他宏。如果宏是通过解释实现的,这将导致解释器的塔,其中第一个解释器本身将解释一个解释器代码,该代码可能解释另一个解释器,依此类推。

  • 如果拼接数等于引号数,则代码将被编译并像往常一样运行。

  • 如果引号数超过拼接数,则代码将被分阶段。也就是说,它在运行时生成类型化抽象语法树或类型结构。超过一个的引号过剩对应于多阶段编程。

为整个语言提供解释器非常困难,让该解释器高效运行甚至更困难。因此,我们目前对拼接的使用施加了以下限制。

  1. 顶层拼接必须出现在内联方法中(将该方法变成宏)

  2. 拼接必须调用先前编译的方法,传递引用的参数、常量参数或内联参数。

  3. 不允许嵌套拼接(但允许中间引号)。

API

迄今为止讨论的框架允许对代码进行分阶段处理,即准备在稍后的阶段执行。要运行该代码,类 Expr 中还有另一个名为 run 的方法。请注意,$run 都将 Expr[T] 映射到 T,但只有 $跨阶段安全性约束,而 run 只是一个普通方法。scala.quoted.staging.run 提供了一个 Quotes,可用于显示其作用域中的表达式。另一方面,scala.quoted.staging.withQuotes 提供了一个 Quotes,而不计算表达式。

package scala.quoted.staging

def run[T](expr: Quotes ?=> Expr[T])(using Compiler): T = ...

def withQuotes[T](thunk: Quotes ?=> T)(using Compiler): T = ...

创建启用分阶段的新 Scala 3 项目

sbt new scala/scala3-staging.g8

来自 scala/scala3-staging.g8

它将创建一个包含必要依赖项和一些示例的项目。

如果您希望自己创建项目,请确保在 build.sbt 构建定义中定义以下依赖项

libraryDependencies += "org.scala-lang" %% "scala3-staging" % scalaVersion.value

如果您直接使用 scalac/scala,则对两者使用 -with-compiler 标志

scalac -with-compiler -d out Test.scala
scala -with-compiler -classpath out Test

示例

现在,完全按照 中的示例进行操作。假设我们不想静态传递数组,而是在运行时生成代码并传递值(也在运行时)。请注意,我们如何在下面第 6 行中创建一个类型为 Expr[Array[Int] => Int] 的未来阶段函数。使用 staging.run { ... },我们可以在运行时计算表达式。在 staging.run 的作用域内,我们还可以对表达式调用 show 以获取表达式的源代码表示。

import scala.quoted.*

// make available the necessary compiler for runtime code generation
given staging.Compiler = staging.Compiler.make(getClass.getClassLoader)

val f: Array[Int] => Int = staging.run {
  val stagedSum: Expr[Array[Int] => Int] =
    '{ (arr: Array[Int]) => ${sum('arr)}}
  println(stagedSum.show) // Prints "(arr: Array[Int]) => { var sum = 0; ... }"
  stagedSum
}

f.apply(Array(1, 2, 3)) // Returns 6