本文概述
创建语言有多种可能的原因, 其中一些原因并非立即显而易见。我想将它们与一种使Java虚拟机(JVM)的语言尽可能重用现有工具的方法一起介绍。这样, 我们将减少开发工作, 并提供用户熟悉的工具链, 从而使采用我们的新编程语言更加容易。
在本系列文章的第一篇中, 我将概述为JVM创建自己的编程语言所涉及的策略和各种工具。在以后的文章中, 我们将深入介绍实现细节。
为什么要创建JVM语言?
已经有无数种编程语言。那么, 为什么还要创建一个新的呢?有许多可能的答案。
首先, 有许多种不同的语言:你要创建通用编程语言(GPL)还是特定领域的语言?第一类包括Java或Scala之类的语言:旨在针对大量问题编写足够体面的解决方案的语言。相反, 领域特定语言(DSL)专注于很好地解决一组特定问题。想想HTML或Latex:你可以在屏幕上绘图或用Java生成文档, 但这会很麻烦, 而使用这些DSL则可以非常轻松地创建文档, 但它们仅限于该特定领域。
因此, 也许存在一些你经常处理的问题, 对于创建DSL来说可能有意义。一种语言, 可以一劳永逸地解决相同类型的问题, 同时使你的工作效率更高。
也许相反, 你想创建GPL是因为你有一些新想法, 例如将关系表示为头等公民或表示上下文。
最后, 你可能想要创建一种新的语言, 因为它既有趣又酷, 而且你将在此过程中学到很多东西。
事实是, 如果你以JVM为目标, 则可以减少工作量以获得可用的语言, 这是因为:
- 你只需要生成字节码, 你的代码就可以在所有有JVM的平台上使用
- 你将能够利用JVM现有的所有库和框架
因此, 在JVM上大大降低了开发语言的成本, 并且在JVM之外不经济的情况下创建新语言是有意义的。
你需要什么使它可用?
你绝对需要使用某些工具来使用语言-这些工具中包括解析器和编译器(或解释器)。但是, 这还不够。为了使你的语言在实践中真正可用, 你需要提供工具链的许多其他组件, 并可能与现有工具集成。
理想情况下, 你希望能够:
- 管理从其他语言为JVM编译的代码的引用
- 使用语法突出显示, 错误识别和自动完成功能在你最喜欢的IDE中编辑源文件
- 你希望能够使用自己喜欢的构建系统来编译文件:maven, gradle或其他
- 你希望能够编写测试并将其作为”持续集成”解决方案的一部分运行
如果可以的话, 采用你的语言会容易得多。
那么我们如何实现呢?在本文的其余部分, 我们将探讨实现这一目标所需的不同部分。
解析与编译
在程序中转换源文件所需要做的第一件事是解析它们, 以获取代码中包含的信息的抽象语法树(AST)表示形式。到那时, 你将需要验证代码:是否存在语法错误?语义错误?你需要找到所有这些并将它们报告给用户。如果一切顺利, 你仍然需要解析符号。例如, “列表”是指java.util.List还是java.awt.List?调用重载方法时, 你正在调用哪个方法?最后, 你需要为程序生成字节码。
因此, 从源代码到编译后的字节码, 共有三个主要阶段:
- 建立一个AST
- 分析和转换AST
- 从AST产生字节码
让我们详细了解这些阶段。
构建AST:解析是一种已解决的问题。有很多框架, 但是我建议你使用ANTLR。它是众所周知的, 维护良好的并且具有一些使指定语法更容易的功能(它处理较少的递归规则-你无需理解, 但要感谢它!)。
分析和转换AST:编写类型系统, 验证和符号解析可能具有挑战性, 并且需要大量工作。仅此主题将需要一个单独的帖子。现在, 请考虑这是编译器的一部分, 你将在其中花费大部分精力。
从AST产生字节码:实际上这最后一个阶段并不难。你应该在上一阶段解析符号并准备好地形, 以便基本上可以将转换后的AST的单个节点转换为一个或几个字节码指令。控制结构可能需要做一些额外的工作, 因为你将要以一系列有条件和无条件的跳转来转换for循环, 开关, ifs等(是的, 在你的优美语言之下, 仍然会有很多问题)。你需要学习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编译或gradle汇编, 并且不会注意到任何差异。
坏消息是编写Maven插件并不容易:文档非常差, 不易理解且大多已过时或完全错误。是的, 听起来不舒服。我尚未编写gradle插件, 但这似乎要容易得多。
请注意, 你还应该考虑如何使用构建系统来运行测试。为了支持测试, 你应该考虑一个非常基本的单元测试框架, 并将其与构建系统集成, 以便运行maven测试可以查找你所用语言的测试, 然后编译并运行它们以将输出报告给用户。
我的建议是看一下可用的示例:其中之一是都灵编程语言的Maven插件。
实施后, 每个人都应该能够轻松编译以你的语言编写的源文件, 并将其用于Travis等持续集成服务中。
IDE插件
IDE的插件将是对用户最可见的工具, 并且这会极大地影响你对语言的理解。一个好的插件可以通过提供智能的自动完成功能, 上下文错误和建议的重构来帮助用户学习语言。
现在, 最常见的策略是选择一个IDE(通常是Eclipse或IntelliJ IDEA)并为其开发特定的插件。这可能是你的工具链中最复杂的部分。出现这种情况的原因有几个:首先, 你无法合理地重复使用将要为另一个IDE开发插件的工作。你的Eclipse和IntelliJ插件将完全分开。第二点是IDE插件开发不是很常见, 因此没有太多的文档, 社区也很小。这意味着你将不得不花费大量时间自己解决问题。我亲自为Eclipse和IntelliJ IDEA开发了插件。我在Eclipse论坛上提出的问题已经有好几个月没有回答了。在IntelliJ论坛上, 我比较幸运, 有时我会得到开发人员的答复。但是, 插件开发人员的用户基础较小, API非常拜占庭式。准备受苦。
所有这些都有另一种选择, 那就是使用Xtext。 Xtext是用于为Eclipse, IntelliJ IDEA和Web开发插件的框架。它诞生于Eclipse, 最近才被扩展以支持其他平台, 因此虽然经验不足, 但值得一提。让我直截了当:开发一个非常好的插件的唯一方法是使用每个IDE的本机API进行开发。但是, 使用Xtext, 你只需付出一小部分努力就可以拥有相当不错的东西-你只需将其提供给你语言的语法, 即可免费获得语法错误/完成。尽管如此, 你仍然必须实现符号解析和困难的部分, 但这是一个非常有趣的起点。但是, 最难的是与平台特定的库集成来解决Java符号, 因此这并不能真正解决你的所有问题。
结论
你可以通过多种方式失去对你的语言表现出兴趣的潜在用户。采用新语言是一项挑战, 因为它需要学习它并适应我们的发展习惯。通过尽可能减少损耗并利用用户已知的生态系统, 可以防止用户在学习并爱上你的语言之前就放弃。
在理想情况下, 你的用户可以克隆一个用你的语言编写的简单项目, 并使用标准工具(Maven或Gradle)来构建它, 而无需注意任何区别。如果他想编辑该项目, 则可以在其最喜欢的编辑器中将其打开, 该插件将帮助你指出错误并提供精巧的完成方式。这与必须弄清楚如何使用记事本调用编译器和编辑文件的情况大不相同。语言周围的生态系统确实可以发挥作用, 如今, 可以通过合理的努力来构建它。
我的建议是要以你的语言而不是工具来发挥创造力。通过使用熟悉的标准来减少人们采用你的语言所面临的最初困难。
语言设计愉快!
评论前必须登录!
注册