Gleam 编译器源码导读多 crate 架构、编译流水线与快照测试实践【免费下载链接】gleam⭐️ A friendly language for building type-safe, scalable systems!项目地址: https://gitcode.com/GitHub_Trending/gl/gleamGleam 是一门面向类型安全、可扩展系统的友好编程语言本仓库即其官方编译器的完整源码。本文以 docs/compiler/README.md 为骨架结合仓库内真实的 Rust 源码、Makefile 与测试工程系统讲解 Gleam 编译器的整体结构、从源码到 Erlang/JavaScript 输出的编译流程以及围绕快照snapshot测试建立的工程实践。读完本文你将能够快速定位编译器各阶段对应的源码文件理解依赖排序、类型推断、代码生成等核心环节的实现位置并掌握在本仓库中运行与扩展测试的方法。项目结构分工清晰的 Rust 多 crate 架构Gleam 编译器不是单个巨型二进制而是由若干 Rust crate在 Cargo 语境下即项目组成的多 crate 工作区。每一层职责单一、边界明确这是理解整个代码库的第一把钥匙。四个核心 Rust crate根据 docs/compiler/README.md 的划分编译器主要由四个 crate 构成crate职责源码入口compiler-core解析、分析、编译 Gleam 项目完全纯函数、无任何 IOcompiler-core/src/lib.rslanguage-serverGleam 语言服务器自动补全、代码操作code actions、悬停提示等language-server/src/lib.rscompiler-cli命令行界面包装核心编译器与语言服务器提供文件与控制台 IOcompiler-cli/src/lib.rscompiler-wasm通过 WebAssembly 暴露给 JavaScript 的核心编译器接口可在浏览器中运行compiler-wasm/src/lib.rs纯核心 IO 外壳是这套架构最重要的设计原则compiler-core只负责把代码变成什么不关心代码从哪来、结果写到哪。所有与文件系统、标准输入输出、网络相关的操作都被推到compiler-cli等外围 crate 中。这样做带来的直接好处是核心逻辑可以脱离具体运行环境被单元测试、被 wasm 复用也能在语言服务器中安全调用而不阻塞 IO。在 compiler-core/src/lib.rs 中可以看到该 crate 导出的全部模块从parse、analyse、type_、erlang、javascript、metadata、docs到build几乎一一对应编译流水线的各个阶段同时文件顶部用#![deny(unsafe_code)]、#![deny(clippy::unwrap_used)]等 lint 规则严格要求代码安全性与健壮性——对编译器这种需要长期演进的基础设施而言这种约束直接服务于可靠性。仓库中的其他组件除了 Rust 代码仓库根目录还包含以下配套组件Makefile为常见开发任务定义快捷命令运行make help可查看全部目标详见下文测试一节。test/一系列大部分为Gleam 工程充当编译器的集成测试夹具例如 test/language、test/project_erlang、test/project_javascript。deny.tomlcargo-deny的配置用于保证编译器依赖的 Rust 库符合项目预期。containers/用于为每个 Gleam 发布版本构建 OCI 容器的 Dockerfile 集合例如 containers/erlang.dockerfile、containers/node.dockerfile。.github/workflows/GitHub Actions 工作流定义负责构建、测试与发布新版本。docs/即本文所在目录存放编译器相关的开发文档。编译流水线从源码到 Erlang/JavaScriptGleam 编译器把模块编译成 Erlang 或 JavaScript 的过程在概念上是一条清晰的多级流水线。原文档给出了下面的流程示意图它也是理解 compiler-core 内部各模块协作关系的最佳地图Gleam source code .cache binaries ▼ ▼ ┌────────────────────┐ ┌───────────────────────┐ │ Parser │ │ Metadata deserializer │ └────────────────────┘ └───────────────────────┘ │ │ Untyped AST Module metadata └─────────┐ ┌────────┘ │ ▼ ▼ │ ┌─────────────────────┐ │ │ Dependency sorter │ │ └─────────────────────┘ │ │ │ Untyped AST │ (sorted by deps) │ ▼ │ ┌───────────────────┐ │ │ Type checker │◄─────┘ └───────────────────┘ │ ┌────── Typed AST ──────┐ ▼ ▼ ┌────────────────────┐ ┌─────────────────────┐ │ Code generator │ │ Metadata serializer │ └────────────────────┘ └─────────────────────┘ │ │ │ ▼ Erlang or JavaScript .cache binaries printing algebra ▼ ┌────────────────────┐ │ Pretty printer │ └────────────────────┘ │ ▼ Erlang or JavaScript source code下面把每个阶段对应到仓库中的真实实现。阶段一解析Parser——源码到 Untyped AST输入 Gleam 源码输出未类型化 ASTUntyped AST这一阶段由 compiler-core/src/parse.rs 完成。文件头部的注释详细记录了语法设计约定parse_x解析某个语法片段失败时不报错通常返回ResultOptionA, ParseErrorexpect_x解析某个通用/特定语法片段失败时报错返回ResultA, ParseErrormaybe_x解析通用语法片段成功则推进 token 流并返回Some(x)否则返回None。表达式与 guard 中的运算符优先级采用Simple Precedence Parser 算法维护表达式栈与未归约运算符栈两个栈边消费输入边比较运算符优先级从而支持e :: expr op expr | expr这样运算符与表达式交替的通用文法。词法层面由 compiler-core/src/parse/lexer.rs 与 compiler-core/src/parse/token.rs 支撑。阶段二依赖排序Dependency sorter依赖排序位于编译器前端负责把模块按依赖关系排成拓扑序保证被依赖的模块先被处理。核心实现在 compiler-core/src/dep_tree.rs 的toposort_deps函数中它基于petgraph图库构建依赖图若图中存在环import cycle则返回Error::Cycle并利用find_cycle回溯出具体的循环路径供后续错误诊断使用。对应的模块级测试toposort_deps_test等与集成测试用例如 test-package-compiler/cases/import_cycle、test-package-compiler/cases/import_cycle_multi共同验证了环检测与多模块环的报错行为。阶段三类型检查Type checker依赖排序后的 Untyped AST 进入类型检查器得到已类型化 ASTTyped AST。这一阶段由 compiler-core/src/analyse.rs 与 compiler-core/src/type_ 模块共同实现。从ModuleAnalyzerConstructor::infer_modulecompiler-core/src/analyse.rs的实现可以看到类型推断的典型执行顺序校验模块名合法性构建EnvironmentArguments当前包名、Gleam 版本、目标平台、可导入模块等先处理 import使任何位置都能引用被导入的内容注册自定义类型使构造函数与函数可在模块中更早的位置被使用对类型别名做拓扑排序后逐一注册注册各函数的签名再逐个语句做类型推断。此外.cache中已编译模块的元数据会通过元数据反序列化器Metadata deserializer读回以ModuleInterface形式为类型检查提供跨模块的类型信息。阶段四代码生成与元数据序列化分叉点拿到 Typed AST 后编译器兵分两路代码生成器面向目标语言生成代码。Erlang 后端位于 compiler-core/src/erlang.rs核心结构为Generator与FunctionGeneratorJavaScript 后端位于 compiler-core/src/javascript.rs。两个后端都以打印代数printing algebra的形式表达生成结果——先把 AST 转换为结构化文档而不是直接拼接字符串。元数据序列化器将模块接口ModuleInterface序列化为.cache二进制供后续编译读取。实现在 compiler-core/src/metadata.rspub fn encode(module: ModuleInterface) - ResultVecu8, bitcode::Error { bitcode::serialize(module) } pub fn decode(bytes: [u8], ids: UniqueIdGenerator) - ResultModuleInterface, bitcode::Error { bitcode::deserialize(bytes).map(|module| remap_type_variable_ids(module, ids)) }其中decode在反序列化后会调用remap_type_variable_ids重新映射类型变量 ID避免跨模块缓存加载时产生 ID 冲突。阶段五Pretty printer——最终源码输出代码生成器产出的打印代数文档最终交给 pretty printer 排版输出可读的 Erlang 或 JavaScript 源码。这种结构化文档 → 排版的两段式设计正是 Gleam 生成的 Erlang/JavaScript 代码始终具有良好缩进与一致风格的原因。命令行入口与纯核心的对接以上纯逻辑全部位于compiler-core而真正的程序入口在 gleam-bin/src/main.rs调用 compiler-cli由 CLI 负责把磁盘上的.gleam源码喂给核心、把生成的源码写回文件系统。这也再次印证了核心纯函数、外壳负责 IO的架构原则。测试三层测试体系与快照测试运行测试的 Makefile 目标原文档定义了 6 个测试命令仓库根目录的 Makefile 有对应实现命令实际执行内容make test运行所有测试先cargo test --quiet跑 Rust 单元测试再依次跑test/language、test/javascript_prelude、test/project_erlang、test/project_javascript、test/project_deno、test/hextarball、test/typescript_declarations、test/running_modules、test/subdir_ffi等集成测试。提交任何改动前都建议跑一遍make test-watch文件保存时自动运行 Rust 单元测试watchexec -e rs,toml,gleam,html cargo test --quietmake language-test运行 test/language 下的跨平台语言集成测试make language-test-watch文件保存时自动运行上述语言集成测试make javascript-prelude-test对编译到 JavaScript 时使用的 Gleam prelude 实现compiler-core/templates/prelude.mjs运行单元测试make javascript-prelude-test-watch文件保存时自动运行 prelude 测试所有*-watch命令都依赖watchexec程序需要先安装。此外 Makefile 还提供了make help列出全部目标、make buildrelease 构建、make install把编译器安装到 PATH等开发辅助命令。以make language-test为例其真实逻辑在 test/language/Makefile依次对 Erlang、JavaScriptNode.js 运行时、JavaScriptDeno 运行时三个目标运行cargo run --quiet -- test --target ... --runtime ...即同一套 Gleam 语言测试用例分别在三个后端/运行时上执行验证跨平台一致性。而make javascript-prelude-test则会先把 compiler-core/templates/prelude.mjs 复制到测试目录再执行node main.mjs测试结束后清理临时文件。快照测试Snapshot testing编译器大量使用基于cargo-insta的快照测试这是本仓库最值得借鉴的测试实践。与传统示例式测试同时手写输入与期望输出不同快照测试只需编写输入工具会把首次运行产生的输出标记为 accepted 并保存为.snap文件此后若某条测试的输出发生变化即视为测试失败开发者可以选择拒绝reject新版本——说明结果出错了接受accept新版本——将其作为新的正确输出写入仓库。这套机制对编译器开发尤其有价值当修改错误消息格式或代码生成输出格式时手动更新所有期望输出既耗时又枯燥而快照测试只需几秒即可完成全量更新。仓库中的快照文件数量庞大且分布广泛例如解析器compiler-core/src/parse/snapshots266 个Erlang 代码生成compiler-core/src/erlang/snapshots67 个及其 tests 子目录JavaScript 代码生成compiler-core/src/javascript/tests834 个快照类型检查compiler-core/src/type_/snapshots32 个与 tests语言服务器language-server/src/tests/snapshots1511 个文档生成compiler-core/src/docs/snapshots47 个日常开发中推荐的工作流如下# 运行全部测试 make test # 交互式核对快照变更 cargo insta reviewcargo insta review会逐个展示变更的快照让开发者在确认无误后统一接受将改动固化进仓库。结语从整体看Gleam 编译器是一个纯核心 IO 外壳的多 crate 工程compiler-core以无 IO 的纯函数形式完成解析、依赖排序、类型检查与双后端代码生成compiler-cli、language-server、compiler-wasm分别以 CLI、LSP、Wasm 三种形态复用这一核心快照测试则让编译器在输出格式频繁演进时仍能保持高度可控。对希望深入编译器实现或为 Gleam 贡献代码的开发者来说docs/compiler/README.md 是绝佳的起点本文对应梳理的源码路径则可以作为后续阅读的导航图。【免费下载链接】gleam⭐️ A friendly language for building type-safe, scalable systems!项目地址: https://gitcode.com/GitHub_Trending/gl/gleam创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考