Gleam v1.19.0发布:重构Erlang代码生成器

Gleam v1.19.0发布,重构Erlang代码生成器。新方案改为生成抽象形式,显著提升构建速度并精准调试信息。基准测试显示全新编译耗时大幅降低,且消除了对Gleam仅是转译工具的误解。

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 美元。团队正努力完善核心成员的薪酬体系,欢迎更多用户考虑赞助该项目或核心团队成员。

评论 0

0/500

评论需审核后展示,请文明发言

💬
还没有评论,来说两句

相关阅读