创建可用的 JVM 语言:概述
已发表: 2022-03-11创建一种语言有几个可能的原因,其中一些不是很明显。 我想将它们与一种方法一起介绍,以便为 Java 虚拟机 (JVM) 开发一种尽可能多地重用现有工具的语言。 通过这种方式,我们将减少开发工作并提供用户熟悉的工具链,从而更容易采用我们的新编程语言。
在本系列的第一篇文章中,我将概述为 JVM 创建我们自己的编程语言所涉及的策略和各种工具。 在以后的文章中,我们将深入探讨实现细节。
为什么要创建你的 JVM 语言?
已经有无数种编程语言。 那么为什么要费心创造一个新的呢? 对此有很多可能的答案。
首先,有许多不同种类的语言:您想创建通用编程语言 (GPL) 还是特定领域的语言? 第一种包括像 Java 或 Scala 这样的语言:旨在为大量问题编写足够体面的解决方案的语言。 相反,领域特定语言 (DSL) 专注于很好地解决一组特定的问题。 想想 HTML 或 Latex:您可以在屏幕上绘图或用 Java 生成文档,但这会很麻烦,而使用这些 DSL,您可以非常轻松地创建文档,但它们仅限于特定领域。
因此,也许有一组您经常工作的问题,并且为这些问题创建 DSL 可能是有意义的。 一种语言,可以让你在一遍又一遍地解决相同类型的问题时非常高效。
相反,也许您想创建一个 GPL,因为您有一些新想法,例如将关系表示为一等公民或表示上下文。
最后,您可能想要创建一种新语言,因为它很有趣、很酷,而且您将在此过程中学到很多东西。
事实是,如果您以 JVM 为目标,您可以轻松获得可用的语言,这是因为:
- 您只需要生成字节码,您的代码将在所有有 JVM 的平台上可用
- 您将能够利用 JVM 现有的所有库和框架
因此,在 JVM 上开发语言的成本大大降低,在 JVM 之外不经济的场景中创建新语言可能是有意义的。
你需要什么使它可用?
您绝对需要一些工具来使用您的语言——解析器和编译器(或解释器)就在这些工具中。 然而,这还不够。 为了使您的语言在实践中真正可用,您需要提供工具链的许多其他组件,可能与现有工具集成。
理想情况下,您希望能够:
- 管理对从其他语言编译为 JVM 的代码的引用
- 使用语法高亮、错误识别和自动完成功能在您喜欢的 IDE 中编辑源文件
- 您希望能够使用您最喜欢的构建系统编译文件:maven、gradle 或其他
- 您希望能够编写测试并将其作为持续集成解决方案的一部分运行
如果你能做到这一点,采用你的语言会容易得多。
那么我们怎样才能做到这一点呢? 在这篇文章的其余部分,我们将研究实现这一目标所需的不同部分。
解析和编译
在程序中转换源文件需要做的第一件事是解析它们,获得代码中包含的信息的抽象语法树 (AST) 表示。 此时您需要验证代码:是否存在语法错误? 语义错误? 您需要找到所有这些并将它们报告给用户。 如果一切顺利,您仍然需要解析符号。 例如,“List”是指java.util.List还是java.awt.List ? 当你调用一个重载的方法时,你调用的是哪一个? 最后,您需要为您的程序生成字节码。
因此,从源代码到编译的字节码,主要分为三个阶段:
- 构建 AST
- 分析和转换 AST
- 从 AST 生成字节码
让我们详细了解这些阶段。
构建 AST :解析是一种已解决的问题。 有很多框架,但我建议你使用 ANTLR。 它是众所周知的,维护良好,并且它具有一些使指定语法变得更容易的功能(它处理较少的递归规则 - 你不需要理解它,但要感谢它!)。
分析和转换 AST :编写类型系统、验证和符号解析可能具有挑战性并且需要大量工作。 仅此主题就需要单独的帖子。 现在考虑这是您的编译器的一部分,您将花费大部分精力。
从 AST 生成字节码:最后一个阶段实际上并不难。 您应该在前一阶段解析符号并准备地形,以便基本上可以将转换后的 AST 的单个节点转换为一个或几个字节码指令。 控制结构可能需要一些额外的工作,因为您将在一系列条件和无条件跳转中转换您的 for 循环、开关、if 等(是的,在您漂亮的语言下面仍然会有一堆 goto)。 您需要了解 JVM 内部是如何工作的,但实际实现并不难。
与其他语言集成
当你的语言统治世界时,所有代码都将专门使用它编写。 但是,作为中间步骤,您的语言可能会与其他 JVM 语言一起使用。 也许有人会开始在一个更大的项目中用您的语言编写几个类或一个小模块。 期望能够混合多种 JVM 语言是合理的。 那么,它如何影响您的语言工具呢?
您需要考虑两种不同的情况:

- 您的语言和其他语言存在于单独编译的模块中
- 您的语言和其他语言位于相同的模块中并一起编译
在第一种情况下,您的代码只需要使用用其他语言编写的编译代码。 例如,一些依赖项(如 Guava)或同一项目中的模块可以单独编译。 这种集成需要两件事:首先,您应该能够解释由其他语言生成的类文件,以将符号解析为它们并生成用于调用这些类的字节码。 第二点与第一点不同:其他模块可能希望在编译后重用用您的语言编写的代码。 现在,通常这不是问题,因为 Java 可以与大多数类文件进行交互。 但是,您仍然可以设法编写对 JVM 有效但不能从 Java 调用的类文件(例如,因为您使用在 Java 中无效的标识符)。
第二种情况更复杂:假设您有一个用 Java 代码定义的 A 类和一个用您的语言编写的 B 类。 假设这两个类相互引用(例如 A 可以扩展 B 并且 B 可以接受 A 作为同一方法的参数)。 现在的重点是 Java 编译器无法处理您的语言中的代码,因此您必须为 B 类提供一个类文件。但是要编译 B 类,您需要插入对 A 类的引用。所以您需要做的是拥有一种部分 Java 编译器,它给定一个 Java 源文件能够解释它并生成它的模型,您可以使用它来编译您的 B 类。请注意,这要求您能够解析 Java 代码(使用JavaParser 之类的东西)和解决符号。 如果您不知道从哪里开始,请查看 java-symbol-solver。
工具:Gradle、Maven、测试框架、CI
好消息是,您可以通过为 gradle 或 maven 开发插件,使他们使用以您的语言编写的模块对用户完全透明。 您可以指示构建系统以您的编程语言编译文件。 用户将继续运行 mvn compile 或 gradle assemble 而不会注意到任何差异。
坏消息是编写 Maven 插件并不容易:文档很差,难以理解,而且大多过时或完全错误。 是的,这听起来并不令人欣慰。 我还没有写过 gradle 插件,但看起来要容易得多。
请注意,您还应该考虑如何使用构建系统运行测试。 对于支持测试,您应该考虑一个非常基本的单元测试框架,并且应该将其与构建系统集成,以便运行 maven 测试以您的语言查找测试,编译并运行它们,将输出报告给用户。
我的建议是查看可用的示例:其中之一是都灵编程语言的 Maven 插件。
一旦你实现了它,每个人都应该能够轻松地编译用你的语言编写的源文件,并在 Travis 等持续集成服务中使用它。
IDE 插件
IDE 的插件将是您的用户最明显的工具,并且会极大地影响您对语言的感知。 一个好的插件可以通过提供智能自动完成、上下文错误和建议的重构来帮助用户学习语言。
现在,最常见的策略是选择一个 IDE(通常是 Eclipse 或 IntelliJ IDEA)并为其开发特定的插件。 这可能是您的工具链中最复杂的部分。 出现这种情况有几个原因:首先,您无法合理地重用您为一个 IDE 开发插件而为其他 IDE 所花费的工作。 您的 Eclipse 和 IntelliJ 插件将完全分开。 第二点是IDE插件开发不是很常见的东西,所以文档不多,社区小。 这意味着您将不得不花费大量时间为自己弄清楚事情。 我个人为 Eclipse 和 IntelliJ IDEA 开发了插件。 我在 Eclipse 论坛上的问题几个月或几年都没有得到解答。 在 IntelliJ 论坛上我运气更好,有时我会从开发人员那里得到答案。 然而,插件开发者的用户群较小,API 非常拜占庭式。 准备受苦。
所有这一切都有一个替代方案,那就是使用 Xtext。 Xtext 是一个为 Eclipse、IntelliJ IDEA 和 web 开发插件的框架。 它是在 Eclipse 上诞生的,并且最近刚刚扩展以支持其他平台,因此在这方面没有太多经验,但它可能是一个值得考虑的替代方案。 让我直截了当地说:开发一个非常好的插件的唯一方法是使用每个 IDE 的本机 API 来开发它。 但是,使用 Xtext,您只需花费很少的精力就可以获得相当不错的东西 - 您只需将其提供给您的语言的语法,您就会免费获得语法错误/完成。 尽管如此,您必须实现符号解析和困难的部分,但这是一个非常有趣的起点; 然而,难点在于与平台特定库的集成以解决 Java 符号,因此这并不能真正解决您的所有问题。
结论
您可能会通过多种方式失去对您的语言表现出兴趣的潜在用户。 采用一种新语言是一项挑战,因为它需要学习它并适应我们的开发习惯。 通过尽可能减少损耗并利用用户已知的生态系统,您可以防止用户在学习并爱上您的语言之前就放弃。
在理想情况下,您的用户可以克隆一个用您的语言编写的简单项目,并使用标准工具(Maven 或 Gradle)构建它,而不会注意到任何差异。 如果他想编辑项目,他可以在它最喜欢的编辑器中打开它,插件将帮助他指出错误并提供智能补全。 这是一个与必须弄清楚如何调用编译器和使用记事本编辑文件大不相同的场景。 围绕您的语言的生态系统确实可以发挥作用,如今可以通过合理的努力来构建它。
我的建议是在你的语言中发挥创造力,而不是在你的工具中。 通过使用熟悉的标准,减少人们在采用您的语言时必须面对的初始困难。
快乐的语言设计!
进一步阅读 Toptal 工程博客:
- 如何从头开始编写解释器
