Gleam 不再编译为 Erlang 源码
摘要
Gleam v1.19.0 发布了重写的 Erlang 代码生成器,不再输出 Erlang 源码,而是输出 Erlang 抽象形式(abstract forms),显著提升了构建速度、崩溃报告的行号准确性以及编译器代码质量。
<p><a href="https://lobste.rs/s/2svplr/gleam_doesn_t_compile_erlang_source">评论</a></p>
查看缓存全文
缓存时间: 2026/10/05 19:15
# Gleam 不再编译为 Erlang 源代码 | Gleam 编程语言
来源:https://gleam.run/news/gleam-doesnt-compile-to-erlang-source-anymore/
Gleam 是一种类型安全、可扩展的语言,面向 Erlang 虚拟机和 JavaScript 运行时。今天 Gleam v1.19.0(https://github.com/gleam-lang/gleam/releases/tag/v1.19.0)已经发布,让我们来看看有哪些新内容。
## 一个全新的编译目标
过去几个月里,[Giacomo Cavalieri](https://github.com/giacomocavalieri) 已经完全重写了 Gleam 的 Erlang 代码生成器。新设计截然不同,最引人注目的是,它输出的格式也变了。以前 Gleam 生成 Erlang 源代码,现在它生成 *Erlang 抽象形式*(https://www.erlang.org/doc/apps/erts/absform.html)。"Erlang 抽象形式"是 Erlang 编译器使用的一种中间表示,它是带元数据标注的树,用于表示 Erlang 语法,通常由运行 Erlang 的词法分析器和解析器产生。它采用 Erlang 的 *外部术语格式*(https://www.erlang.org/doc/apps/erts/erl_ext_dist.html)进行二进制编码,借助这种二进制格式,我们可以直接加载生成的代码,跳过 Erlang 编译器的前端部分。
这个全新的 Erlang 代码生成器带来了几项好处:
- 编译器性能得到提升,显著缩短了在 Erlang 上运行的 Gleam 项目的构建时间。
- 运行时可用的位置信息现在精确对应原始 Gleam 源代码,而不再是编译器生成的 Erlang 源代码。这意味着,例如 BEAM 崩溃报告和堆栈跟踪中的行号现在完全准确,而以前这些信息可能是不准确的,只能指向最近的函数。这些元数据还有可能让 [edb](https://github.com/WhatsApp/edb) 等调试器完整支持 Gleam,尽管我们自己并没有为此做任何工作。
- Gleam 编译器的代码质量得到了提升。Erlang 代码生成器曾是 Gleam 代码库中最古老、最稳定的部分之一,虽然它没有给我们带来任何问题,但它并不符合我们如今的标准和惯例。这个全新的替代品非常出色,可以说提升了整个编译器的水准。
- 我们再也不用听到有人把 "transpiler"(转译器)这个词当贬义词用了。[^1]
## 好吧,那到底有多快?
我马上会展示一些数据,但请记住,基准测试总是人为构造的,永远无法说明全貌。这些数据可以作为一个很好的入门或起点,但要真正理解,还需要自己进一步研究和实践。
这个[基准测试](https://github.com/lpil/langcompilebench)基于 José Valim 的 `langcompilebench` 项目。谢谢你,José!它测量的是编译 100 个模块所需的时间,每个模块包含 100 个返回 "hello world" 字符串的函数。这种做法很实用,因为这种形态的测试项目可以很容易地在不同语言之间复制,从而产出最接近的同类测试项目,但它的局限性在于,每种语言只有一小部分特性被编译了。在真实项目中,代码的多样性要大得多,而且不同的特性在不同语言中的编译开销也各不相同。
代码生成器重写的第一阶段是在 v1.18.0(上一个版本)中发布的,所以让我们比较一下 v1.17.0 和新发布的 v1.19.0。下图显示了编译基准测试项目所用的时间,越低越好。
0ms 100ms 200ms 300ms 400ms 500ms
Gleam v1.19
Gleam v1.17
如你所见,改进相当可观!这是完全从零开始的构建,没有任何缓存。Gleam 的编译是增量式的,因此在日常开发过程中会快得多,因为它不会编译整个项目。
原始的 `langcompilebench` 只包含 Erlang、Elixir 和 Gleam,但我添加了各种其他流行的编程语言,帮助大家大致感受一下 Gleam 的编译速度相比熟悉的语言有多快。我还包含了 Gleam 编译到 JavaScript 的结果。以下是结果:
0ms 200ms 400ms 600ms 800ms 1000ms
Gleam (JavaScript)
Go
Gleam (Erlang)
Erlang
Java
Elixir
Elm
Rust
C#
TypeScript
7
请记住:这是一个人为构造的基准测试,仅凭这一点不足以对这些语言得出任何确凿的结论。话虽如此,这些结果确实表明 Gleam 的编译相当快速。作为一名 Gleam 程序员,我可以说 Gleam 的开发体验非常愉快,几乎不需要花时间等待计算机。
## 为什么不直接以 BEAM 字节码为目标?
我们已经从"将源代码交给 Erlang 编译器"转移到了"将中间表示交给 Erlang 编译器",但为什么不干脆绕过 Erlang 编译器呢?我们难道不能做出一个性能超过 Erlang 编译器的 BEAM 字节码生成器吗?也许我们还可以利用 Gleam 的类型信息来生成更优化的代码。
虽然这些好处确实可以实现,但不太可能由我们来实现。与 Erlang 源代码和 Erlang 抽象形式不同,BEAM 字节码并不是固定不变的。虚拟机的每个新版本都可能演进和改进字节码,添加新功能,有时也会移除那些已被冗余化的功能。我们需要承诺永远跟上这种演进,与 Erlang 维护者紧密合作,以便为即将到来的变化做好准备,并确保在虚拟机发布新版本时,Gleam 也能有新版本就绪。此外,要重现 Erlang 编译器几十年来实现的所有现有优化,即便有 Gleam 更强大的静态分析作为助力,也将是一项巨大的工作量。
Gleam 是一个由[赞助](https://gleam.run/sponsor/)支持的社区项目。我们的财力只有那些由企业或学术机构支持的语言的一小部分,因此我们必须仔细考虑最高效、最可持续的方式来使用我们的资源。Gleam 是软件开发的可靠基础,我们做出的每一个决定都必须经得起未来数年乃至数十年的考验。
编译为 Erlang 抽象形式是当前 Gleam 在成本与收益之间的最佳平衡点。在这一点上,我们也与优秀的语言为伍。我们深爱的老大哥语言 Elixir 同样是通过抽象形式编译到 Erlang 的。如果对 Elixir 来说足够好,那么对 Gleam 也足够好!
好了,关于这个就说这么多。Gleam v1.19.0 中还有很多其他内容值得介绍。
## JavaScript 决策树赋值优化
得到改进的不只是 Erlang 代码生成,JavaScript 方面也有一些不错的改进。在 Gleam 中,流程控制是通过 `case` 表达式进行模式匹配来完成的,编译后会变成嵌套的 `if` 语句。由于模式匹配是声明式的,编译器能够重新排列和优化运行时逻辑,采用分而治之的方法尽快找到正确的分支。John Downey(https://github.com/jtdowney)改进了这一过程,生成更扁平的代码,将嵌套的 `if` 语句合并为单个条件,并减少中间变量。
例如,看这段 Gleam 代码:
```
pub fn go(x) {
case x {
Wibble(1, 2) -> 1
_ -> 2
}
}
```
以前,这一小段 Gleam 会编译成下面这一大段出人意料的 JavaScript[^2]:
```
export function go(x) {
if (isWibble(x)) {
let $ = x[0];
if ($ === 1) {
let $1 = x[1];
if ($1 === 2) {
return 1;
} else {
return 2;
}
} else {
return 2;
}
} else {
return 2;
}
}
```
但现在它生成的是:
```
export function go(x) {
if (isWibble(x) && x[0] === 1 && x[1] === 2) {
return 1;
} else {
return 2;
}
}
```
相信你也同意,这是一个不错的改进!令人意外的是,经过压缩和打包后,这对代码包的大小几乎没有影响(gzip 真的是太神奇了),但生成的代码分支更少,JavaScript 引擎可以更高效地优化。谢谢你,John!
## 列表字面量优化
尽管它们在各自语言中的语法相似,Gleam 的不可变持久化列表类型与 JavaScript 的可变连续数组类型并不相同。当 Gleam 代码编译到 JavaScript 时,任何列表字面量都必须编译成构造运行时数据结构的 JavaScript 代码。例如,看这段 Gleam 代码:
```
let numbers = [1, 2, 3]
```
这会编译成类似下面这样的 JavaScript 代码[^2],其中会构造一个 JavaScript 数组,并将其传递给一个函数来转换为 Gleam 列表:
```
const numbers = arrayToList([1, 2, 3])
```
在这个版本中,编译器现在会为较短的列表字面量生成不同的代码,生成更直接的代码,不再进行数组转换:
```
const numbers = prepend(1, prepend(2, prepend(3, empty)))
```
在现代 JavaScript 引擎上,这带来了可观的性能提升,对于大量使用短列表的项目来说尤其如此,比如使用 [Lustre](https://lustre.hexdocs.pm/) 库的项目。我们记录到,对于较长的列表没有改进,因此这些情况仍然使用数组到列表的转换方式。
感谢 [Giacomo Cavalieri](https://github.com/giacomocavalieri) 做出的这项贡献!
## TypeScript API 重载
在编译到 JavaScript 时,Gleam 编译器还会生成[函数](https://gleam.run/documentation/externals/#Gleam-data-in-JavaScript),以便在 JavaScript 中操作程序员定义的数据结构。除此之外,编译器还可以提供 TypeScript 声明文件,从而在同一个项目中实现 TypeScript 与 Gleam 的完全集成。
编译器为每个自定义类型提供的函数之一,是用于检查某个值是否属于特定变体的函数。例如,给定以下类型:
```
pub type Box(value) {
Full(value)
Empty
}
```
生成的函数会有这样的 TypeScript 声明:
```
export function Box$isFull(value: any): value is Full;
```
眼尖的 TypeScript 程序员读者可能会注意到这里有问题。如果一个值已知类型是 `Box`,那么这个函数可以用来将容器类型精化为 `Full`,但 `number` 这个类型参数却被泛化为 `unknown`,导致类型信息丢失。这非常麻烦。
[Giacomo Cavalieri](https://github.com/giacomocavalieri) 为这个定义添加了重载,以便在可能的情况下保留类型信息:
```
export function Box$isFull(value: Box$): value is Full;
export function Box$isFull(value: any): value is Full;
```
谢谢你,Giacomo!
Gleam 用户通常会使用 `gleam` 可执行文件中附带的官方构建工具,但有时人们希望在其他环境中编译和使用 Gleam 代码。例如,Elixir 或 Erlang 程序员可能希望使用一个用 Gleam 编写的依赖包。在运行时,这三门 BEAM 语言之间有着出色的零成本互操作,效果非常棒,但要走到这一步可能会很棘手,因为 Elixir 和 Erlang 的主要构建工具都没有内置对 Gleam 的支持。
`gleam` 可执行文件提供了多个命令来暴露编译器功能,供其他构建工具使用。这个版本包含多项对这些命令的改进,目标是让 Elixir 的 Mix 和 Erlang 的 rebar3 获得 Gleam 支持。
Erlang 虚拟机要求所有包除了编译器字节码外,还要有 `.app` 资源文件。以前,支持 Gleam 的构建工具需要自己提供这些文件,但现在 `gleam` 在编译到 BEAM 时会为它们生成这些文件。
`compile-package` 命令增加了一个 `--no-dev` 标志,它会让编译器只加载 `src` 目录中的代码,并跳过列为 `dev_dependencies` 的包。`export package-information` 和 `export package-interface` 命令现在可以将信息打印到标准输出,而以前它们必须写入文件。此外,`export javascript-prelude` 和 `export typescript-prelude` 命令现在也可以写入文件。
感谢 [Rodrigo Álvarez](https://github.com/Papipo) 做出的这些改进!希望我们能很快看到 Elixir 的 Mix 构建工具支持 Gleam。
## 语言服务器对标签的支持
Gleam 内置了一个出色的语言服务器,为所有支持语言服务器协议的编辑器提供 IDE 功能。可能最后一项主要功能是完整支持字段和参数的标签。[Alistair Smith](https://github.com/alii) 修复了这一点,添加了对标签的跳转到定义、查找引用和重命名的支持!
谢谢你,Alistair,我知道很多人会为这个节省时间的功能感到非常高兴。
## 在浏览器中格式化 Gleam 代码
编译器有一个 WebAssembly 构建版本,被[语言之旅](https://tour.gleam.run/)和[练习场](https://playground.gleam.run/)用于在网页浏览器中编译 Gleam。[John Downey](https://github.com/jtdowney) 添加了一个新的 `format_source` 函数,使人们可以在浏览器中运行 Gleam 代码格式化器。我们将在不久的将来把这个功能添加到练习场中。谢谢你,John!
## 像往常一样,更好的错误信息
让错误信息尽可能清晰、尽可能有帮助,对我们来说非常重要。一个工具在一切顺利时好用固然很好,但当事情变得糟糕时,使用体验才能真正帮助(或伤害)程序员的压力水平。小的意外语法错误可能会让人很痛苦,尤其是当你不确定错误是什么、错在哪里的时候。
[0xda157](https://github.com/0xda157) 添加了一种特殊错误,当代码中发现 git 合并冲突标记时会触发;还添加了另一种错误,当记录更新语法 `User(..lucy, score: 10)` 中的原始记录写在了错误位置,比如写成 `User(score: 10, ..lucy)` 时会触发。她还为 Gleam 中不存在的过程式运算符(如 `+=` 和 `*=`)添加了错误。
[n0kk23](https://github.com/n0kk23) 添加了一个自定义的友好错误信息,当在模式匹配中以 Gleam 中不合法但在 Java 等其他语言中合法的方式使用 `|` 时会触发。
[Giacomo Cavalieri](https://github.com/giacomocavalieri) 为不允许在常量表达式中出现的二元运算符添加了友好错误信息,同时他还提高了编译器在遇到这类错误时的容错性[^3]。
[Andrey Kozhev](https://github.com/ankddev) 为当模块试图使用同一包中另一个模块的私有类型或值时的错误信息添加了额外的上下文,告知用户该类型或值确实存在,但是私有的。我们不会为依赖包中的模块提供这种提示,以避免泄露程序员并不维护的代码信息。
最后,[James Dolan](https://github.com/jamesdolan16) 改进了类型检查器,使得无效的类型别名定义不会再通过该别名的所有使用处引发级联错误。
感谢大家让 Gleam 的调试变得越来越轻松。
## 其他内容
还要感谢所有修复 bug 和打磨体验的人:[0xda157](https://github.com/0xda157)、[Amr Kadry](https://github.com/Amrkadry)、[Andrey Kozhev](https://github.com/ankddev)、[Giacomo Cavalieri](https://github.com/giacomocavalieri)、[Hari Mohan](https://github.com/seafoamteal)、[Ian Chamberlain](https://github.com/ian-h-chamberlain)、[Jack Programs](https://github.com/jackprogramsjp)、[John Downey](https://github.com/jtdowney)、[Mar Bloeiman](...)、Lillian Rose(https://github.com/lillianrubyrose)……
[^1]: 是的,这可能不太公平,因为 "transpiler" 并不是一个真正带有贬义的词。但确实有人是这样用的。
[^2]: 这里的代码经过了简化以便展示。
[^3]: 容错性是指编译器在遇到错误时仍能继续生成尽可能多的有效错误信息,而不是直接崩溃或停止。
相似文章
Gleam v1.17.0
Gleam v1.17.0 引入了 `gleam export escript` 命令以创建单文件 BEAM 程序,在语言服务器中高亮引用,以及常量 `todo` 表达式。此外,首届 Gleam Gathering 大会的视频也已发布。
对Gleam编译器的模糊测试
本文探讨了模糊测试技术,包括基于LLM的和结构感知模糊测试,以发现Gleam编译器中的错误,该编译器可以编译为JavaScript和Erlang,并具有静态类型。
Gleam 语言服务器的字段盛宴 | Gleam v.1.18.0 发布
Gleam 1.18.0 版本已发布,其语言服务器进行了重大改进,包括对记录字段的支持、类型变量重命名和模块重命名,以及通过数据单例实现更快的 JavaScript 代码生成。
Gleam 已将其源代码镜像到 tangled(一个基于 AT 协议的代码托管平台)
Gleam 是一种类型安全的编程语言,已将其源代码镜像到 tangled,这是一个基于 AT 协议构建的代码托管平台。
Gleam的酷炫之处
作者分享了学习Gleam编程语言的个人笔记,重点介绍了@deprecated属性、todo构造、泛型和case表达式等特性,并与其他语言进行了比较。