Metacarp

Lobsters Hottest 工具

摘要

Metacarp 是一个为 Carp 编程语言打造的新编译器,它本身用 Carp 编写,具备模块化库、LLVM 后端以及增量式 JIT 编译功能。

<p><a href="https://lobste.rs/s/3nk1lh/metacarp">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/09/13 04:45

# Metacarp 来源:https://blog.veitheller.de/Metacarp.html 过去几个月,我一直在为Carp (https://github.com/carp-lang/Carp) 编写一个新的编译器。它被称为 Metacarp (https://github.com/cyberwitchery/metacarp),因为它用 Carp 编写,并且能编译 Carp——而且我更擅长写代码而不是取名字。显然,这不是第一个 Carp 编译器。参考实现是用 Haskell 编写的,已经为我们服务了大约十年。我很早之前写过关于它的东西 (https://blog.veitheller.de/The_Carp_Compiler_%28as_of_2017%29.html)。那篇帖子现在够年龄上小学了,描述的是一种相当不同的语言。自那以后,我们对 Carp 进行了大量改进,编译器和库生态系统都已今非昔比。只需看看现在的 Carpentry (https://carpentry.dev/),你就会发现许多库都能满足你的需求。Metacarp 是该语言的第二个实现。它能编译参考测试套件,能编译自身,然后再次编译自身,生成字节级完全相同的 C 代码。它被拆分为独立的编译器阶段库,拥有第二个 LLVM 后端,能在输入之间保持编译器会话存活,并且可以将代码增量编译到正在运行的 JIT 中。它还不是一个可以立即替换的版本。它的诊断信息不同,部分实现相当古怪,一些程序仍然能找到令人兴奋的“跌落边缘”的方式。所以,这就是公告。让我们四处看看吧! ## 编译器 Metacarp 首先是一个普通的命令行编译器。你使用参考实现来构建它: `` carp -b --optimize main.carp `` 会得到 `out/carp-compiler`。将这个程序指向一个 Carp 程序,它会生成一个 C 翻译单元: `` ./out/carp-compiler -c examples/squares.carp `` 或者要求它调用 Clang 并直接运行结果: `` ./out/carp-compiler -x -c examples/squares.carp `` 这个例子加载了常规的 Carp 标准库,展开其宏,推断并特化程序,检查其所有权,生成 C 代码,构建它,并打印从一到十的偶数的平方和。没错,为了打印 `220` 而动用这么多机制确实有点多,但这也是相当有趣的! Metacarp 理解所有让实现 Carp 变得麻烦的东西:编译时语言和宏、Hindley-Milner 类型推断、接口、单态化、模式匹配、闭包以及所有权和借用规则。它为托管值所需推导复制器和删除器,并在生成 C 代码前插入对它们的调用。它可以加载现有的 Core 库并编译现有的程序。它能在 Carpentry 库 (https://carpentry.dev/) 上工作。虽然还没有 100% 兼容,但那些行为不符合预期的地方,也是你在使用旅程中很晚才会遇到的东西。 ## 编译器即库 命令行只是使用编译器的方式之一。在我撰写本文时,Metacarp 大约有 42,000 行 Carp 代码,分布在实现各个阶段的库中: `` source registry -> module loading -> surface parsing -> macro expansion -> name resolution -> type inference -> specialization -> ownership planning -> backend lowering -> C `` 这是一个相当标准的编译器流水线。有趣的部分在于,这些是实际的库边界。它们有自己的数据模型、入口点、测试和错误。你可以只使用解析器而不使用类型检查器,只使用推断器而不使用后端。你可以在没有 LLVM 或 C 的情况下运行所有权规划(是的,LLVM 也存在,下文会讲到)。我不打算描述每一个阶段,但你的代码代理可能会称每一个阶段都是“承重”的。它们相互衔接,但尝试进行合理的划分。每个边界都返回结构化信息。这是什么意思?这意味着解析失败会包含源码位置信息,只有命令行驱动程序会将其转换为其下的终端输出。一切都可以在机器层面处理这些信息。我觉得这非常酷!这也意味着,你可以在流水线的任何点停止,检查发生了什么,或者将编译器用于不是“获取这个文件并给我一个可执行文件”的用途。自然,我这么做了,毕竟我是个离经叛道的家伙。 ## 编译器作为宠物 `carp-session` 库让一个编译器保持存活。创建一个会话会一次性加载、展开、解析、推导和推断 Core 库。在这个不可变的基础之上,它维护着一组由其客户端提供的定义。一个笔记本单元可以临时地根据这两者进行检查,而无需成为会话的一部分: `` Core,一次加载 + 已提交的定义 + 这个单元 `` 客户端可以提交一个定义、替换它或删除它。当定义发生变化时,会话会找到依赖于它的定义,并重建受影响的视图。失败的更改不会毒害编译器,候选状态会被丢弃,而最后已知良好的状态仍然可用。这赋予了笔记本式代码预期的行为。一个单元可以定义一个函数: `` (defn twice [x] (* x 2)) `` 另一个单元可以询问它的类型,使用它的补全信息,编译 `\(twice 21\)`。第二个单元不需要将第一个单元粘贴在前面,也不需要重新加载标准库。它就是在那里。会话 API 会用定义、类型、诊断信息、文档、补全、展开和相对于源码的位置信息来回答。它还有钩子,可以在推断后、所有权规划后或降级后停止。这就是 gt4carp (https://github.com/carpentry-org/gt4carp) 背后的机制,其中 Lepiter 页面获得一个温暖的编译器会话,代码片段表现得像一个不断演进的小程序的一部分。我们将来会谈论那个。以这种方式描述有状态编译听起来很明显。但对我来说,实现它却一点也不明显。批量编译可以推断一个封闭的程序,使用其求解器状态,然后丢弃一切。一个持久会话则结合不同运行产生的事实,并在各代之间重用它们。有一段时间这工作得很完美,直到后面一个单元使得一个之前未使用的多态定义变得可达,此时特化发现了一个类型变量,而其替换信息在几次请求前就已消失。如果这句话让你一头雾水,欢迎来到我的世界。第一个正确的修复使得在 Metacarp 本身上编译一个单元大约需要三十秒。缓存不可变部分后,在同一台机器上降到了大约两百毫秒。先让它运行,然后再让它快——这是最棒的。我至今仍惊讶它竟然真的成功了! ## 原生编译器 常规后端生成 C 代码。还有一个 LLVM 后端,它消费相同的降级后的程序,包括相同的所有权规划,但生成的是 LLVM IR。需要明确指出,这不是一个玩具路径。LLVM 驱动在参考测试套件上与 C 驱动具有同等地位,包括数组、字符串、闭包、泛型和类型、全局变量、模式匹配、派生的内存操作以及 Core 使用的 C 模板。它可以生成目标文件并链接一个正常的程序,但更有趣的客户端是 JIT。这就是“编译器即一组库”这一赌注完全生效的魔力所在。会话 JIT 在语义编译器会话旁边维护着一个 LLVM 上下文和 ORC 实例。定义及其机器码在单元之间得以存活。后面的单元只需要特化和降级那些新变得可达的定义,生成一个小的 LLVM 模块,并将其符号发布到正在运行的进程中。如果发布失败,该模块可以再次被移除,而不会破坏之前的单元。在我机器上使用真实的 Core 库,第一个单元大约需要一秒,一个热单元大约需要 60 毫秒。等效的“生成 C、调用 Clang、运行”路径每个单元大约需要 380 毫秒。这一切都是本地的,数字可能很糟糕,但趋势绝对不是。对我来说,这是神奇的部分。编译器是用 Carp 编写的。编译器会话和 JIT 驱动也是用 Carp 编写的。被编译的程序是 Carp。而且所有自举的代际最终都会汇聚并生成逐字节相同的输出。计算机有时很好,尤其是当它们不还嘴的时候。 ## Bug 在这里 Metacarp 可以使用,但它尚未完成——对于编译器而言,“完成”到什么程度都很难说。它将宿主架构和操作系统硬编码到构建路径中,因此交叉编译主要还是一种愿景。它的删除位置基于作用域而非活跃性,这可能会让大值停留的时间超过必要。在一个控制流路径上被消耗然后在另一个路径上重新赋值的绑定,仍然可能泄漏重新赋值的值。某些缓存是进程全局的,当多个 JIT 客户端认为它们是独立的时候,这尤其有趣。如果这些听起来很严重,再次欢迎来到我的世界。诊断信息是它自己的。有些比参考编译器的更好,有些更差,而且没有一个是字节兼容的。它们肯定没有我希望的那么好或那么友好。该实现接受参考测试套件中的程序,但称之为“直接替换”会引诱我的读者在读完这句话之前就找出反例。请务必去看看。很多人在使用它并觉得很有趣,而且我能处理错误报告。 ## 结束语 Metacarp 是一个自举的 Carp 编译器,一组编译器库,一个温暖的增量编译器服务,一个 C 编译器,一个 LLVM 编译器,以及一个 JIT。它编译现有的语言和 Core,而不是一个为演示而发明的、礼貌的小子集。它可以编译一个文件,回答关于一个笔记本单元的问题,或者将一个原生函数交给一个正在运行的程序。它也能轻松地成就或毁掉我的夜晚。构建它给我带来了巨大的乐趣。它已经达到了可以在其上构建其他东西的程度,而且我确实在做,相当多。你可以在 GitHub 上找到它 (https://github.com/cyberwitchery/metacarp)。它很酷,很神奇,很怪异,也充满 bug。对于一个新的、用于像 Carp 这样酷、神奇且怪异的语言的编译器来说,这似乎是一个很好的状态。

相似文章

微型语言实现:Calcium

Lobsters Hottest

一篇关于Calcium的博客文章,Calcium是一个微型编程语言实现,旨在演示像Python这样的语言是如何编译和执行的,由Ned Batchelder创建,以解释编译型与解释型之间的争论。

第一个Futamura投影

Lobsters Hottest

这篇博客文章解释了如何通过将解释器特化到程序来实现第一个Futamura投影,并以一个Brainfuck到Carp的编译器作为实际例子。