此文档页面特定于 Scala 3,并且可能涵盖 Scala 2 中不可用的新概念。除非另有说明,此页面中的所有代码示例均假定你使用的是 Scala 3。
如果你创建一个名为 Hello.scala 的 Scala 3 源代码文件
@main def hello = println("Hello, world")
然后使用 scalac
编译该文件
$ scalac Hello.scala
你会注意到,在其他结果文件中,scalac
创建了扩展名为 .tasty 的文件
$ ls -1
Hello$package$.class
Hello$package.class
Hello$package.tasty
Hello.scala
hello.class
hello.tasty
这导致了一个自然的问题,“Tasty 是什么?”
什么是 TASTy?
TASTy 是一个缩写词,源自术语 Typed Abstract Syntax Trees。它是 Scala 3 的高级交换格式,在本文档中,我们将称其为 Tasty。
首先要知道的是,Tasty 文件是由 scalac
编译器生成的,并且包含有关你的源代码的所有信息,包括程序的语法结构以及关于类型、位置甚至文档的完整信息。Tasty 文件包含的信息比在 JVM 上运行而生成的 .class 文件多得多。(稍后会详细介绍。)
在 Scala 3 中,编译过程如下所示
+-------------+ +-------------+ +-------------+
$ scalac | Hello.scala | -> | Hello.tasty | -> | Hello.class |
+-------------+ +-------------+ +-------------+
^ ^ ^
| | |
Your source TASTy file Class file
code for scalac for the JVM
(contains (incomplete
complete information)
information)
你可以通过使用 -print-tasty
标志在编译器上运行 .tasty 文件来以人类可读的形式查看其内容。你还可以使用 -decompile
标志以类似于 Scala 源代码的形式查看反编译的内容。
$ scalac -print-tasty hello.tasty
$ scalac -decompile hello.tasty
.class 文件的问题
由于 类型擦除 等问题,.class 文件实际上是代码的不完整表示。演示这一点的一个简单方法是使用 List
示例。
类型擦除 意味着当您编写如下 Scala 代码时,该代码应在 JVM 上运行
val xs: List[Int] = List(1, 2, 3)
该代码被编译为一个 .class 文件,该文件需要与 JVM 兼容。由于该兼容性要求,该类文件中的代码(您可以使用 javap
命令查看)最终看起来像这样
public scala.collection.immutable.List<java.lang.Object> xs();
该 javap
命令输出显示了类文件中所包含内容的 Java 表示形式。请注意在此输出中,xs
不再 被定义为 List[Int]
;它本质上表示为 List[java.lang.Object]
。为了让您的 Scala 代码与 JVM 配合使用,Int
类型已被擦除。
稍后,当您在 Scala 代码中访问 List[Int]
的元素时,如下所示
val x = xs(0)
生成的类文件将对此代码行进行强制转换操作,您可以将其视为如下所示
int x = (Int) xs.get(0) // Java-ish
val x = xs.get(0).asInstanceOf[Int] // more Scala-like
同样,这样做是为了兼容性,因此您的 Scala 代码可以在 JVM 上运行。但是,我们已经拥有整数列表的信息在类文件中丢失了。在尝试针对已编译的库编译 Scala 程序时,这会带来问题。为此,我们需要比类文件中通常可用的更多信息。
而且,此讨论仅涵盖类型擦除的主题。对于 JVM 不了解的每个其他 Scala 构造,都有类似的问题,包括联合、交集、带参数的特征以及更多 Scala 3 特性。
TASTy 的救援
因此,TASTy 格式在类型检查后存储了完整的抽象语法树 (AST),而不是在 .class 文件中不包含有关原始类型的任何信息,或仅包含公共 API(如 Scala 2.13 “Pickle” 格式)。存储整个 AST 具有很多优势:它支持独立编译、为不同的 JVM 版本重新编译、对程序进行静态分析,以及更多功能。
要点
因此,这是本节的第一个要点:你在 Scala 代码中指定的类型在 .class 文件中并未完全准确地表示。
第二个要点是要理解在 编译时 和 运行时 可用的信息之间存在差异
- 在 编译时,当
scalac
读取并分析你的代码时,它知道xs
是List[Int]
- 当编译器将你的代码写入类文件时,它会写入
xs
是List[Object]
,并且在xs
被访问的其他所有地方添加类型转换信息 - 然后在 运行时——你的代码在 JVM 中运行——JVM 不知道你的列表是
List[Int]
对于 Scala 3 和 Tasty,这里有另一个关于编译时的重要说明
- 当你编写使用其他 Scala 3 库的 Scala 3 代码时,
scalac
不必再读取它们的 .class 文件;它可以读取它们的 .tasty 文件,如前所述,它们是你的代码的精确表示。这对于支持 Scala 2.13 和 Scala 3 之间的独立编译和兼容性非常重要。
Tasty 的好处
正如你所想象的,拥有代码的完整表示有很多 好处
- 编译器使用它来支持独立编译。
- 基于 Scala 语言服务器协议 的语言服务器使用它来支持超链接、命令补全、文档,以及全局操作,如查找引用和重命名。
- Tasty 为新一代 基于反射的宏奠定了良好的基础。
- 优化器和分析器可以使用它进行深度代码分析和高级代码生成。
在相关说明中,Scala 2.13.6 有一个 TASTy 读取器,Scala 3 编译器还可以读取 2.13 “Pickle” 格式。Scala 3 迁移指南 中的 类路径兼容性页面 解释了这种跨编译功能的好处。
更多信息
总之,Tasty 是 Scala 3 的高级交换格式,.tasty 文件包含源代码的完整表示,从而带来上一节中概述的好处。
有关更多详细信息,请参阅以下资源
- 在 此视频 中,Scala Center 的 Jamie Thompson 全面讨论了 Tasty 的工作原理及其优势
- 面向库作者的二进制兼容性 讨论了二进制兼容性、源兼容性和 JVM 执行模型
- Scala 3 过渡的前向兼容性 演示了在同一项目中使用 Scala 2.13 和 Scala 3 的技术
这些文章提供了有关 Scala 3 宏的更多信息
TASTyViz 是一个用于直观检查 TASTy 文件的工具。在撰写本文时,它仍处于开发的早期阶段,因此你可能会遇到缺失的功能和不太理想的用户体验,但它在调试时仍然可能很有用。你可以在 此处 查看它。