资讯动态

Gleam 编译器已知痛点清单深度解析:JavaScript 打包、编译期工作分配与自定义类型表示优化

发布时间:2026/9/13 21:03:07 来源:尧图企业网站定制
Gleam 编译器已知痛点清单深度解析JavaScript 打包、编译期工作分配与自定义类型表示优化【免费下载链接】gleam⭐️ A friendly language for building type-safe, scalable systems!项目地址: https://gitcode.com/GitHub_Trending/gl/gleam本篇技术指南聚焦 Gleam 编译仓库内的 docs/annoyances.md 这份已知痛点清单逐条剖析其中记录的三个悬而未决的技术问题编译后 JavaScript 的打包不够直观、重复工作无法迁移到编译期或初始化期、以及 JavaScript 后端自定义类型的运行时表示仍有性能优化空间。通过对照 compiler-core/src/javascript.rs、compiler-core/templates/prelude.mjs 与 compiler-core/src/config.rs 等源码实现读者可以理解这些问题产生的底层原因、当前编译器的真实处理方式以及未来可能的设计方向。文档定位一份编译器维护者的优化路线图在 Gleam 仓库中docs/annoyances.md是一份特殊的技术文档它既不是用户手册也不是变更日志而是编译器维护者亲手记录的、在编写 Gleam 代码过程中遇到并希望未来解决的设计性痛点清单。文档开篇明确说明This document contains a list of issues and annoyances that we have writing Gleam code today, so that we can devise solutions to them in future.本文档记录了我们在编写 Gleam 代码时遇到的各类问题与不便之处以便未来为它们设计解决方案。同时文档也划定了边界已经存在已知解决方案、但尚未实现的痛点不会记录在这份文档里而是跟踪在 Gleam 的 issue 跟踪器中。这意味着docs/annoyances.md只收录那些**问题清晰、方案尚不明确**的设计课题——它实质上是一份面向编译器设计者的开放问题清单每个条目都代表一个值得深入研究的优化方向。目前文档中记录着三个主题Bundling compiled JavaScript is non-obvious打包编译后的 JavaScript 不够直观Cannot shift repeated work to compile or initialise time无法将重复性工作转移到编译期或初始化期JavaScript custom type representation could be fasterJavaScript 自定义类型表示可以更快下文将逐条展开并结合当前仓库的源码实现说明每个问题的具体背景。痛点一打包编译后的 JavaScript 不够直观现状每个 Gleam 模块编译为一个独立 ESM 模块Gleam 的 JavaScript 后端位于 compiler-core/src/javascript.rs采用逐模块编译的策略每个.gleam源文件被编译成独立的.mjs文件模块之间通过 ES Moduleimport/export互相引用。从Generator::compile的组装顺序javascript.rs可以清晰看到每个输出文件的完整结构sourcemap 引用sourcemap_reference当开启源码映射时输出文件末尾会附带//# sourceMappingURLmodule.mjs.map注释TypeScript 声明引用type_reference当开启 TypeScript 声明生成时会附带对./module.d.mts的引用导入语句imports.into_doc由collect_imports收集模块自身的 import、外部函数 FFI 导入以及按需注册的 prelude 函数模块定义definitions依次输出自定义类型、常量、函数echo 定义echo_definition当代码中使用echo调试表达式时注入 echo.mjs 模板。其中最关键的是按需导入 prelude机制Generator内部维护一个UsageTracker在生成表达式代码时记录哪些 prelude 功能被用到如Ok/Error、toList、prepend、isEqual、CustomType、位数组系列函数等最后在compile收尾阶段通过register_prelude_usage为实际用到的功能生成精确的导入语句javascript.rs。这保证了每个.mjs文件只导入自己真正依赖的运行时函数。源码级证据prelude 是唯一的公共运行时所有的 JavaScript 运行时支撑代码集中在一个文件中compiler-core/templates/prelude.mjs它通过include_str!直接嵌入编译器二进制见 javascript.rs 的pub const PRELUDE: str include_str!(../templates/prelude.mjs)。这个文件定义了CustomType基类含withFields方法用于记录更新List类及Empty/NonEmpty子类并实现了Symbol.iterator迭代器、toArray()、atLeastLength()、countLength()等高效辅助方法BitArray类支持非字节对齐的位偏移bitOffset、位切片、整数/浮点读写等UtfCodepoint类以及toList、prepend、isEqual等核心函数。由于每个模块最终都要引用 prelude或其派生出的模块打包器必须能够正确解析这类跨模块的 ESM 依赖关系。为什么打包成为痛点对于一个要发布到浏览器或 Node.js 生产环境的 Gleam 项目开发者需要把几十上百个.mjs文件合并成少量 bundle。痛点在于Gleam 编译器本身不负责打包——从 compiler-cli 的源码结构看CLI 只负责编译、运行与发布产物就是分散的 ESM 文件输出文件包含多种附加引用——sourcemap、.d.mts类型声明引用、按需生成的 prelude 导入打包配置需要理解并正确处理这些引用否则很容易出现打包后类型声明丢失或sourcemap 失效的问题运行时是预编译共享库——prelude.mjs 中Empty、List$Empty$const这类单例常量的语义依赖模块共享打包器的 tree-shaking 或作用域隔离如果处理不当可能破坏单例标识语义。正因如此文档把打包编译后的 JavaScript列为需要改进的体验问题。目前仓库中的做法是项目本身依赖外部打包工具如 esbuild、rollup、webpack 等通用 JS 打包器并通过 gleam.toml 的[javascript]配置段声明运行环境见下节。相关配置gleam.toml 的 [javascript] 段与 JavaScript 打包、运行直接相关的配置项定义在 compiler-core/src/config.rs 的JavaScriptConfig中配置项类型默认值说明typescript_declarationsboolfalse是否生成.d.mtsTypeScript 类型声明文件source_mapsboolfalse是否生成.mjs.map源码映射文件runtimenode/deno/bun/browser等node目标 JavaScript 运行时deno对象—Deno 运行时的权限配置allow_net、allow_read、allow_env、allow_run、allow_write、allow_ffi、allow_all等default_javascript_runtime函数将默认运行时设定为Runtime::NodeJsconfig.rs。一个典型的配置片段如下示例见 config.rs[javascript] typescript_declarations true runtime node [javascript.deno] allow_net true当typescript_declarations true时编译器还会生成prelude.d.mts同样通过include_str!嵌入见 javascript.rs为 prelude 提供类型声明支撑。可行的优化方向从文档的记录方式看这一痛点的理想解法是让编译产物 → 可部署 bundle的路径更直接例如编译器直接内置打包能力或提供一键式打包命令避免用户自行拼装 ESM 依赖链在输出层面减少对打包器的隐式要求如将 prelude 内联进每个模块消除共享模块依赖输出更规范的清单manifest说明模块间依赖关系方便外部打包工具消费。需要注意的是这些都属于推断方向当前仓库尚未实现真正的进展以 issue 跟踪器与 CHANGELOG.md 为准。痛点二无法将重复工作转移到编译期或初始化期问题描述文档第二条痛点的原文只有一句示例说明For example, regex compilation例如正则表达式的编译。含义是在 JavaScript 后端诸如regex.compile(...)这类代价较高的操作如果每次都写在函数体内那么每次调用都会重新执行而 Gleam 编译器目前缺少一种机制让这类结果确定、反复使用的计算提前到编译期由编译器完成或初始化期模块加载时完成一次从而避免运行时重复开销。为什么难以实现从语言与编译器结构推断结合仓库结构可以从三个层面理解这个限制缺乏编译期求值/宏机制Gleam 是一门无宏macro-free语言compiler-core/src/ast中只有常量constant.rs、类型typed.rs/untyped.rs等常规 AST 节点没有面向用户的编译期执行通道。用户无法编写在编译时运行一次的代码常量折叠能力有限虽然存在 compiler-core/src/ast/constant.rs 这样的常量抽象但constant只支持字面量与纯数据构造不包含正则编译这类需要调用运行时函数的操作模块初始化顺序依赖即使把计算推迟到初始化期JavaScript 后端中 prelude 导入、单例常量如List$Empty$const的求值顺序与模块加载顺序javascript.rs 中 imports 先于 statements 输出也决定了初始化期执行必须在依赖就绪之后这进一步提高了方案设计难度。当前可行的权宜做法在语言层支持之前仓库中可以看到几种缓解手段在模块顶层定义常量Gleam 顶层const会被编译为模块级的常量定义module_constant其求值发生在模块初始化阶段而非每次调用天然具备初始化期执行一次的效果使用 FFI 封装有状态的预计算对象例如在test/external_only_javascript与test/external_only_erlang这类测试目录中展示的用法通过external将昂贵的准备步骤放入模块级的 JS 代码中执行一次再暴露为 Gleam 函数。值得期待的演进方向文档把它列为无现成方案的痛点暗示社区未来可能在以下方向探索增加惰性初始化/单例语义让regex.compile这类调用在模块首次使用时只执行一次引入编译期常量求值扩展使纯函数的常量折叠能力更强在标准库层面提供预编译缓存惯用法。以上方向均属推测仓库当前并未实现读者应避免将其当作既有功能。痛点三JavaScript 自定义类型表示可以更快当前表示方式每个变体一个 class这是三个痛点中源码证据最充分的一条。Gleam 的 JavaScript 后端将每个自定义类型的每个构造器variant编译为一个继承CustomType的 JavaScript class。核心生成逻辑在variant_definition、variant_class_definition、variant_constructor_constant等函数中javascript.rs可以总结为以下产物class 定义如class Wibble extends CustomType { ... }构造函数把各字段赋值到this上带标签的字段用this.label无标签的用this[$index]无字段变体的单例常量Type$Variant$const保证同一变体的所有值共享同一引用从而可以用引用相等做快速比较javascript.rs构造器函数export const Type$Variant (arg1, arg2) new Variant(arg1, arg2)类型判断函数export const Type$isVariant (value) value instanceof Variant字段访问函数为每个字段生成Type$Variant$label或Type$Variant$index取值函数并利用TypedCustomType的 accessors 生成跨变体共享字段的 gettershared_custom_type_fields。prelude 中的基类CustomType.withFields负责记录更新record update它枚举实例自身属性用传入的字段覆盖后构造新实例见 prelude.mjs。已经存在的性能优化值得说明的是当前实现并非毫无优化。从源码可以确认两项已有优化instanceof替代isEqual在模式匹配场景代码生成器会优先输出value instanceof Variant而非isEqual(value, new Variant())因为前者无需构造新对象即可完成判断compiler-core/src/javascript/expression.rs 有明确注释无字段变体单例化variant_constructor_constant的注释指出单例常量让同一变体的所有值共享底层引用从而支持更高效的比较。为什么还可以更快从代码结构推断文档中Would would be optimal?这句反问原文如此表明维护者知道有优化空间但尚未确定什么才是最优表示。从prelude.mjs与生成器代码可以推断出若干开销来源class 实例体积每个带字段的变体都是一个完整 class 实例对象头、原型链查找与字段赋值都有固定开销对象属性访问 vs 数组索引带标签字段通过this.label访问属性名查找尤其是动态属性通常比数组索引慢尽管生成器为无标签字段生成了this[$index]索引访问但标签字段仍走属性路径见variant_class_definition中参数与构造体的分支逻辑javascript.rs类型判断与字段访问的函数包装每个变体都生成Type$isVariant、Type$Variant$field等导出函数调用链比直接访问多一层。对照Erlang 后端的表示方式作为参照Erlang 后端采用了完全不同的表示无字段构造器编译为原子atom带字段构造器编译为以原子为标签的元组如{wibble, Field1, Field2}见 compiler-core/src/erlang.rs 中constructor_atom to_snake_case(constructor.name)与constructor with no fields becomes a regular atom的注释。元组原子在 Erlang 虚拟机上是高度优化的表示访问与比较都非常廉价。由此可以推断JavaScript 后端的理想方向可能是寻找类似紧凑且可快速比较的表示例如基于数组、带整数标签的对象或使用Symbol等但正如文档所述最优方案尚未确定。这些痛点与代码库的呼应docs/annoyances.md虽短但三个主题都贯穿于仓库的各处实现与测试中可作为继续研究的路标JavaScript 后端测试compiler-core/src/javascript/tests 下包含 prelude、布尔值、位数组等大量快照测试snapshot 测试例如tests/prelude.rs验证 prelude 导入与Ok/Error的限定名行为任何表示层面的改动都会在此体现自定义类型表示测试JavaScript 测试目录中的快照文件直接展示了 class、单例常量与instanceof判断的生成结果是观察当前表示最直观的入口打包与运行相关集成测试仓库中的 test/ 目录包含external_only_javascript、project_javascript、javascript_prelude等项目级测试如test/javascript_prelude/Makefile与main.mjs验证编译产物在真实 JS 运行时下的行为配置解析测试compiler-core/src/config.rs 对应的快照测试如gleam_core__config__*系列锁定了[javascript]配置段的 JSON/TOML 序列化行为性能基准benchmark/ 下的基准项目如benchmark/list为List等核心数据结构提供基准测试可用于量化表示方案改动带来的性能变化。结语如何跟进这些痛点docs/annoyances.md是一份活文档每当维护者在日常编码中遇到设计层面的不便就可能追加新条目一旦某个痛点有了明确方案并实现它就会从这份清单中移除转而以功能的形式出现在 CHANGELOG.md 与各版本的变更记录changelog/中。因此跟踪这份文档的变化等同于跟踪 Gleam 编译器在 JavaScript 后端与语言能力层面的演进方向。对于想要深入研究或贡献的读者建议的阅读路径是通读 docs/annoyances.md理解三个开放问题的语境对照 compiler-core/src/javascript.rs 与 compiler-core/templates/prelude.mjs建立Gleam 源码 → JS 产物的映射查看 compiler-core/src/javascript/tests 下的快照测试观察当前生成代码的具体形态若关注 Erlang 对照可阅读 compiler-core/src/erlang.rs 中自定义类型的编译逻辑最终以 issue 跟踪器与 CHANGELOG.md 为准确认各痛点的解决进度避免把本文描述的方向当作已实现功能。【免费下载链接】gleam⭐️ A friendly language for building type-safe, scalable systems!项目地址: https://gitcode.com/GitHub_Trending/gl/gleam创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价