Gleam v1.19.0 发布:Erlang 代码生成器重构与编译性能提升
Gleam 是一款运行在 Erlang 虚拟机和 JavaScript 运行时上的类型安全、可扩展语言。近日,Gleam v1.19.0 版本正式发布,带来了编译器架构的重大调整及多项性能优化。

Erlang 代码生成目标变更
过去几个月,开发者 Giacomo Cavalieri 对 Gleam 的 Erlang 代码生成器进行了彻底重写。此前,Gleam 直接生成 Erlang 源代码;现在,它生成 Erlang 抽象形式(Abstract Form)。
Erlang 抽象形式是 Erlang 编译器使用的中间表示,本质上是一个带有元数据标注、代表 Erlang 语法的树结构。该结构通过 Erlang 外部术语格式进行二进制编码,允许直接加载生成的代码。
这一变更带来以下主要优势:
- 构建速度显著提升:编译器性能得到改进,大幅缩短了 Gleam 项目在 Erlang 平台上的构建时间。
- 调试信息更精准:运行时位置元数据现在准确对应原始 Gleam 源代码,而非中间生成的 Erlang 代码。这意味着 BEAM 崩溃报告、堆栈跟踪中的行号完全准确,且 EDB 等调试器能更好地支持 Gleam。
- 代码质量规范:新实现符合当前的标准和规范,提升了编译器内部代码的质量。
- 消除“翻译器”误解:不再有人将 Gleam 简单描述为一种低级的转译工具。
基准测试表现
基于 José Valim 的 langcompilebench 项目进行的基准测试显示,编译 100 个模块(每个模块包含 100 个返回 "hello world" 字符串的函数)所需时间显著降低。需要注意的是,此类基准测试仅反映特定场景下的性能,实际项目中不同语言特性的编译成本差异较大。
代码生成器重写的第一阶段已在 v1.18.0 中发布。v1.19.0 的数据表明,在无缓存的全新构建场景下,编译时间有明显改善。由于 Gleam 支持增量编译,日常开发中的构建速度会更快。
为了提供横向对比,基准测试还扩展至其他流行编程语言及 Gleam 编译到 JavaScript 的情况。尽管单一基准不足以得出绝对结论,但数据显示 Gleam 的编译效率具有竞争力。
为何选择抽象形式而非字节码?
针对为何不直接生成 BEAM 字节码以追求更高性能的疑问,官方解释如下:
- 稳定性考量:与 Erlang 源代码和抽象形式不同,BEAM 字节码并非固定不变。虚拟机的每次更新都可能改变字节码结构,添加新功能或移除冗余特性。直接生成字节码需要紧密跟随 Erlang 维护团队的变动,增加了维护成本。
- 资源可持续性:Gleam 是由社区赞助支持的项目,需确保资源利用的高效性与长期可持续性。复制 Erlang 编译器数十年来积累的优化工作是一项巨大工程,即便借助 Gleam 的静态分析能力也难以轻松实现。
- 生态一致性:姊妹语言 Elixir 同样通过抽象形式编译为 Erlang。采用相同策略有助于保持生态系统的兼容性和稳定性。
JavaScript 后端优化
v1.19.0 在 JavaScript 编译目标上也进行了多项改进:
决策树分配优化
John Downey 改进了模式匹配的代码生成逻辑。此前,Gleam 的 case 表达式编译为嵌套 if 语句;现在,编译器采用分治法生成更平坦的代码结构,减少嵌套层级和中间变量。
例如,以下 Gleam 代码:
pub fn go(x) {
case x {
Wibble(1, 2) -> 1
_ -> 2
}
}
旧版生成的 JavaScript 代码存在多层嵌套,而新版生成如下更简洁的代码:
export function go(x) {
if (isWibble(x) && x[0] === 1 && x[1] === 2) {
return 1;
} else {
return 2;
}
}
虽然压缩后包大小变化不大,但扁平化的代码结构有利于 JavaScript 引擎进行分支预测和优化。
列表字面量优化
Giacomo Cavalieri 优化了短列表字面量的编译方式。Gleam 的不可变持久列表与 JavaScript 的可变数组不同。对于短列表,编译器现在直接生成构造调用,而非先创建数组再转换。
例如,let numbers = [1, 2, 3] 此前编译为 const numbers = arrayToList([1, 2, 3]),现在编译为:
const numbers = prepend(1, prepend(2, prepend(3, empty)))
这对使用 Lustre 库等频繁操作短列表的项目尤为有益。长列表仍沿用数组转换方法。
TypeScript API 重载
为解决类型信息丢失问题,Giacomo Cavalieri 为生成的 TypeScript 声明文件添加了函数重载。此前,检查值是否为特定变体的函数(如 Box$isFull)会将泛型参数退化为 unknown。现在,定义如下:
export function Box$isFull(value: Box$): value is Full;
export function Box$isFull(value: any): value is Full
这确保了在已知容器类型的情况下,类型参数得以保留,提升了 TypeScript 与 Gleam 集成的体验。
构建工具集成改进
为了更好地支持在 Elixir Mix 和 Erlang rebar3 环境中使用 Gleam,官方构建工具 gleam 命令进行了增强:
- .app 文件生成:当编译到 BEAM 时,Gleam 现在会自动生成 Erlang 所需的 .app 资源文件和编译器字节码,简化了依赖管理。
- --no-dev 标志:
compile-package命令新增此标志,允许仅从 src 目录加载代码并跳过 dev_dependencies。 - 输出重定向:
export-package-info和export-package-interface命令现支持将信息打印到 stdout,而不仅限于写入文件。同时,export-javascript-prelude和export-typescript-prelude支持写入文件。
开发者体验增强
语言服务器标签支持
Alistair Smith 为 Gleam 内置的语言服务器添加了完整的标签支持,包括定义跳转、查找引用和标签重命名功能,进一步提升了 IDE 集成体验。
浏览器端格式化
John Downey 实现了在 WebAssembly 环境中运行 Gleam 代码格式化器的功能。未来,Gleam Playground 将支持在浏览器内直接格式化代码。
错误消息优化
多位贡献者改进了编译器的错误提示,使其更清晰、更具指导性:
- 0xda157:新增检测 Git 合并冲突标记的特殊错误;为不支持的运算符(如 +=, *=)添加明确报错。
- n0kk23:为在其他语言有效但在 Gleam 语法中无效的模式匹配添加自定义错误信息。
- Giacomo Cavalieri:为常量表达式中不允许的二进制运算符添加有用错误消息,并提高编译器容错性。
- Andrey Kozhev:当模块尝试使用同一包内其他模块的私有类型或值时,提供更详细的上下文信息,说明其存在但为私有。
- James Dolan:改进类型检查器,防止无效的别名定义引发连锁错误。
致谢与社区支持
感谢以下贡献者在 bug 修复和体验优化方面的工作:0xda157, Amr Kadry, Andrey Kozhev, Giacomo Cavalieri, Hari Mohan, Ian Chamberlain, Jack Programs, John Downey, Lillian Rose, Mar Bloeiman, mmustafasenoglu, Naomi Roberts, Rodrigo Álvarez, Senthilnathan, Surya Rose, Vivid 等。
Gleam 项目由社区赞助支持,非公司所有。目前大部分赞助者每月贡献 5 至 20 美元。团队正努力完善核心成员的薪酬体系,欢迎更多用户考虑赞助该项目或核心团队成员。





